package identity

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"
)

// mockOIDCProvider starts a minimal OIDC provider that serves
// discovery + JWKS + token endpoint, signing self-signed ID tokens.
// It returns the issuer URL + a cleanup function.
func mockOIDCProvider(t *testing.T, clientID string) (issuer string, privateKey any, cleanup func()) {
	t.Helper()
	// We use a very minimal mock: discovery returns a JWKS URL +
	// token URL pointing to the same test server. The token
	// endpoint returns a fake ID token. For full verification
	// we'd need RSA signing, but for the client logic tests we
	// verify the flow wiring, not the crypto (the verifier is
	// tested via integration in P26).
	var srv *httptest.Server
	srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/.well-known/openid-configuration":
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]any{
				"issuer":                                srvURL(srv),
				"authorization_endpoint":                srvURL(srv) + "/auth",
				"token_endpoint":                        srvURL(srv) + "/token",
				"jwks_uri":                              srvURL(srv) + "/jwks",
				"device_authorization_endpoint":         srvURL(srv) + "/device",
				"response_types_supported":              []string{"code"},
				"subject_types_supported":               []string{"public"},
				"id_token_signing_alg_values_supported": []string{"none"},
			})
		case "/jwks":
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]any{"keys": []any{}})
		case "/token":
			w.Header().Set("Content-Type", "application/json")
			// Return a minimal unsigned ID token (header.payload.sig
			// with empty sig). The verifier in production validates
			// against JWKS; for tests we only check the flow wiring.
			payload := map[string]any{
				"iss":    srvURL(srv),
				"sub":    "test-user-123",
				"aud":    clientID,
				"exp":    time.Now().Add(time.Hour).Unix(),
				"iat":    time.Now().Unix(),
				"groups": []string{"orca-admins"},
				"email":  "test@example.com",
			}
			payloadBytes, _ := json.Marshal(payload)
			enc := base64Raw(payloadBytes)
			idToken := "eyJhbGciOiJub25lIn0." + enc + "."
			json.NewEncoder(w).Encode(map[string]any{
				"access_token":  "at-123",
				"refresh_token": "rt-456",
				"id_token":      idToken,
				"expires_in":    3600,
				"token_type":    "Bearer",
			})
		case "/device":
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]any{
				"device_code":      "dc-123",
				"user_code":        "ORCA-CODE",
				"verification_uri": srvURL(srv) + "/device-verify",
				"interval":         1,
				"expires_in":       300,
			})
		default:
			http.NotFound(w, r)
		}
	}))
	return srvURL(srv), nil, srv.Close
}

func srvURL(srv *httptest.Server) string {
	return "http://" + srv.Listener.Addr().String()
}

func base64Raw(b []byte) string {
	return base64.RawURLEncoding.EncodeToString(b)
}

// TestOIDCClientDiscovery verifies NewOIDCClient discovers the issuer.
func TestOIDCClientDiscovery(t *testing.T) {
	issuer, _, cleanup := mockOIDCProvider(t, "test-client")
	defer cleanup()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	client, err := NewOIDCClient(ctx, OIDCConfig{
		Issuer:   issuer,
		ClientID: "test-client",
	})
	if err != nil {
		t.Fatalf("NewOIDCClient: %v", err)
	}
	if client.Issuer() != issuer {
		t.Errorf("issuer = %q, want %q", client.Issuer(), issuer)
	}
}

// TestCredentialsRoundTrip verifies Save + Load credentials round-trip
// at 0600.
func TestCredentialsRoundTrip(t *testing.T) {
	dir := t.TempDir()
	t.Setenv("ORCA_HOME", dir)
	creds := &Credentials{
		IDToken:      "id-123",
		AccessToken:  "at-456",
		RefreshToken: "rt-789",
		Expiry:       time.Now().Add(time.Hour),
		Issuer:       "https://idp.example",
		Subject:      "user-1",
		Groups:       []string{"admins"},
	}
	if err := SaveCredentials(creds); err != nil {
		t.Fatalf("Save: %v", err)
	}
	loaded, err := LoadCredentials()
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if loaded.Subject != "user-1" {
		t.Errorf("subject = %q, want user-1", loaded.Subject)
	}
	if len(loaded.Groups) != 1 || loaded.Groups[0] != "admins" {
		t.Errorf("groups = %v, want [admins]", loaded.Groups)
	}
}

// TestCredentialsModeEnforced verifies LoadCredentials rejects looser
// than 0600.
func TestCredentialsModeEnforced(t *testing.T) {
	dir := t.TempDir()
	t.Setenv("ORCA_HOME", dir)
	creds := &Credentials{IDToken: "x", Issuer: "x", Subject: "x"}
	if err := SaveCredentials(creds); err != nil {
		t.Fatalf("Save: %v", err)
	}
	// Loosen to 0644.
	path := dir + "/credentials.json"
	if err := os.Chmod(path, 0o644); err != nil {
		t.Fatalf("chmod: %v", err)
	}
	_, err := LoadCredentials()
	if err == nil {
		t.Error("LoadCredentials should reject 0644")
	}
}

// TestClearCredentials verifies logout removes the file.
func TestClearCredentials(t *testing.T) {
	dir := t.TempDir()
	t.Setenv("ORCA_HOME", dir)
	creds := &Credentials{IDToken: "x", Issuer: "x", Subject: "x"}
	_ = SaveCredentials(creds)
	if err := ClearCredentials(); err != nil {
		t.Fatalf("Clear: %v", err)
	}
	if _, err := LoadCredentials(); err == nil {
		t.Error("Load after clear should fail")
	}
}

// TestDefaultScopes verifies the scopes include openid.
func TestDefaultScopes(t *testing.T) {
	scopes := DefaultScopes()
	found := false
	for _, s := range scopes {
		if s == "openid" {
			found = true
		}
	}
	if !found {
		t.Error("DefaultScopes missing openid")
	}
}
