package auth

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"Noooste/garage-ui/internal/config"

	"golang.org/x/oauth2"
)

// newOIDCServerWithTokenAndUserInfo extends the minimal discovery stub with
// token and userinfo endpoints so ExchangeCode and GetUserInfo can be driven
// end-to-end without a real IdP.
func newOIDCServerWithTokenAndUserInfo(
	t *testing.T,
	tokenResp map[string]any,
	tokenStatus int,
	userInfoResp map[string]any,
	userInfoStatus int,
) *httptest.Server {
	t.Helper()
	mux := http.NewServeMux()
	var srv *httptest.Server
	mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
		doc := map[string]any{
			"issuer":                                srv.URL,
			"authorization_endpoint":                srv.URL + "/auth",
			"token_endpoint":                        srv.URL + "/token",
			"jwks_uri":                              srv.URL + "/jwks",
			"userinfo_endpoint":                     srv.URL + "/userinfo",
			"id_token_signing_alg_values_supported": []string{"RS256"},
			"response_types_supported":              []string{"code"},
			"subject_types_supported":               []string{"public"},
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(doc)
	})
	mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"keys":[]}`))
	})
	mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(tokenStatus)
		if tokenResp != nil {
			_ = json.NewEncoder(w).Encode(tokenResp)
		}
	})
	mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(userInfoStatus)
		if userInfoResp != nil {
			_ = json.NewEncoder(w).Encode(userInfoResp)
		}
	})
	srv = httptest.NewServer(mux)
	t.Cleanup(srv.Close)
	return srv
}

func TestExchangeCode_OIDCDisabledReturnsError(t *testing.T) {
	svc := &Service{authConfig: &config.AuthConfig{}, serverConfig: &config.ServerConfig{}}
	if _, err := svc.ExchangeCode(context.Background(), "some-code"); err == nil {
		t.Fatal("expected error when OIDC not initialized")
	}
}

func TestExchangeCode_TokenEndpointErrorPropagates(t *testing.T) {
	srv := newOIDCServerWithTokenAndUserInfo(t,
		map[string]any{"error": "invalid_grant"}, http.StatusBadRequest,
		nil, http.StatusOK,
	)
	svc, err := NewAuthService(
		&config.AuthConfig{OIDC: config.OIDCConfig{
			Enabled:   true,
			ClientID:  "c",
			IssuerURL: srv.URL,
			Scopes:    []string{"openid"},
		}},
		&config.ServerConfig{RootURL: "https://example.test"},
	)
	if err != nil {
		t.Fatalf("NewAuthService: %v", err)
	}

	_, err = svc.ExchangeCode(context.Background(), "bad-code")
	if err == nil {
		t.Fatal("expected error for 400 from token endpoint")
	}
	if !strings.Contains(err.Error(), "failed to exchange code") {
		t.Errorf("error = %v, want wrap 'failed to exchange code'", err)
	}
}

func TestVerifyIDToken_OIDCDisabledReturnsError(t *testing.T) {
	svc := &Service{authConfig: &config.AuthConfig{}, serverConfig: &config.ServerConfig{}}
	if _, err := svc.VerifyIDToken(context.Background(), "tok"); err == nil {
		t.Fatal("expected error when OIDC not initialized")
	}
}

func TestVerifyIDToken_GarbageTokenRejected(t *testing.T) {
	// Discovery works; JWKS is empty so no signature can verify — any token
	// is rejected. This exercises the verifier error path.
	srv := newOIDCServerWithTokenAndUserInfo(t, nil, http.StatusOK, nil, http.StatusOK)
	svc, err := NewAuthService(
		&config.AuthConfig{OIDC: config.OIDCConfig{
			Enabled:   true,
			ClientID:  "c",
			IssuerURL: srv.URL,
			Scopes:    []string{"openid"},
		}},
		&config.ServerConfig{RootURL: "https://example.test"},
	)
	if err != nil {
		t.Fatalf("NewAuthService: %v", err)
	}

	_, err = svc.VerifyIDToken(context.Background(), "not-a-real-jwt")
	if err == nil {
		t.Fatal("expected verifier error for garbage token")
	}
}

func TestGetUserInfo_OIDCDisabledReturnsError(t *testing.T) {
	svc := &Service{authConfig: &config.AuthConfig{}, serverConfig: &config.ServerConfig{}}
	_, err := svc.GetUserInfo(context.Background(), &oauth2.Token{AccessToken: "x"})
	if err == nil {
		t.Fatal("expected error when OIDC not initialized")
	}
}

func TestGetUserInfo_ProviderErrorPropagates(t *testing.T) {
	srv := newOIDCServerWithTokenAndUserInfo(t,
		nil, http.StatusOK,
		map[string]any{"error": "invalid_token"}, http.StatusUnauthorized,
	)
	svc, err := NewAuthService(
		&config.AuthConfig{OIDC: config.OIDCConfig{
			Enabled:   true,
			ClientID:  "c",
			IssuerURL: srv.URL,
			Scopes:    []string{"openid"},
		}},
		&config.ServerConfig{RootURL: "https://example.test"},
	)
	if err != nil {
		t.Fatalf("NewAuthService: %v", err)
	}

	_, err = svc.GetUserInfo(context.Background(), &oauth2.Token{AccessToken: "bad"})
	if err == nil {
		t.Fatal("expected error from userinfo endpoint")
	}
	if !strings.Contains(err.Error(), "failed to get user info") {
		t.Errorf("error = %v", err)
	}
}

func TestGetUserInfo_HappyPath_ExtractsClaims(t *testing.T) {
	srv := newOIDCServerWithTokenAndUserInfo(t,
		nil, http.StatusOK,
		map[string]any{
			"sub":                "user-123",
			"preferred_username": "alice",
			"email":              "alice@example.com",
			"name":               "Alice Example",
			"resource_access": map[string]any{
				"garage": map[string]any{
					"roles": []any{"admin", "user"},
				},
			},
		},
		http.StatusOK,
	)
	svc, err := NewAuthService(
		&config.AuthConfig{OIDC: config.OIDCConfig{
			Enabled:           true,
			ClientID:          "c",
			IssuerURL:         srv.URL,
			Scopes:            []string{"openid"},
			UsernameAttribute: "preferred_username",
			EmailAttribute:    "email",
			NameAttribute:     "name",
			RoleAttributePath: "resource_access.garage.roles",
		}},
		&config.ServerConfig{RootURL: "https://example.test"},
	)
	if err != nil {
		t.Fatalf("NewAuthService: %v", err)
	}

	info, err := svc.GetUserInfo(context.Background(), &oauth2.Token{AccessToken: "good"})
	if err != nil {
		t.Fatalf("GetUserInfo: %v", err)
	}
	if info.Username != "alice" || info.Email != "alice@example.com" || info.Name != "Alice Example" {
		t.Errorf("got %+v, want alice/alice@example.com/Alice Example", info)
	}
	if len(info.Roles) != 2 || info.Roles[0] != "admin" {
		t.Errorf("Roles = %v, want [admin user]", info.Roles)
	}
}

func TestExtractClaim(t *testing.T) {
	cases := []struct {
		name   string
		claims map[string]interface{}
		key    string
		want   string
	}{
		{"empty key returns empty", map[string]interface{}{"a": "b"}, "", ""},
		{"missing key returns empty", map[string]interface{}{"a": "b"}, "missing", ""},
		{"non-string value returns empty", map[string]interface{}{"n": 42}, "n", ""},
		{"string value returned", map[string]interface{}{"email": "x@y"}, "email", "x@y"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := extractClaim(tc.claims, tc.key); got != tc.want {
				t.Errorf("extractClaim(%v, %q) = %q, want %q", tc.claims, tc.key, got, tc.want)
			}
		})
	}
}

func TestGenerateAndValidateStateToken_Roundtrip(t *testing.T) {
	jwtSvc, err := NewJWTService()
	if err != nil {
		t.Fatalf("NewJWTService: %v", err)
	}
	svc := &Service{
		authConfig:   &config.AuthConfig{},
		serverConfig: &config.ServerConfig{},
		jwtService:   jwtSvc,
	}

	tok, err := svc.GenerateStateToken()
	if err != nil {
		t.Fatalf("GenerateStateToken: %v", err)
	}
	if tok == "" {
		t.Fatal("state token is empty")
	}
	if !svc.ValidateAndConsumeState(tok) {
		t.Fatal("ValidateAndConsumeState rejected a freshly-issued token")
	}
	// Double-consume must fail (CSRF single-use).
	if svc.ValidateAndConsumeState(tok) {
		t.Fatal("ValidateAndConsumeState accepted a re-used token")
	}
}

func TestValidateAndConsumeState_RejectsGarbage(t *testing.T) {
	jwtSvc, err := NewJWTService()
	if err != nil {
		t.Fatalf("NewJWTService: %v", err)
	}
	svc := &Service{jwtService: jwtSvc}
	if svc.ValidateAndConsumeState("definitely-not-a-token") {
		t.Fatal("accepted an invalid token")
	}
}
