// Copyright (c) 2024 Warden Project
// SPDX-License-Identifier: MPL-2.0

package jwt

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"context"
	"github.com/stephnangue/warden/auth/helper"
	"net/http"
	"net/http/httptest"
	"strconv"
)

// =============================================================================
// validateBoundClaims Tests
// =============================================================================

func TestValidateBoundClaims_NoBoundClaims(t *testing.T) {
	claims := map[string]interface{}{
		"sub":   "user123",
		"email": "user@example.com",
	}

	err := validateBoundClaims(claims, nil)
	assert.NoError(t, err)
}

func TestValidateBoundClaims_EmptyBoundClaims(t *testing.T) {
	claims := map[string]interface{}{
		"sub": "user123",
	}
	boundClaims := map[string]any{}

	err := validateBoundClaims(claims, boundClaims)
	assert.NoError(t, err)
}

func TestValidateBoundClaims_SingleMatch(t *testing.T) {
	claims := map[string]interface{}{
		"sub": "user123",
		"iss": "https://issuer.example.com",
	}
	boundClaims := map[string]any{
		"iss": "https://issuer.example.com",
	}

	err := validateBoundClaims(claims, boundClaims)
	assert.NoError(t, err)
}

func TestValidateBoundClaims_MultipleMatches(t *testing.T) {
	claims := map[string]interface{}{
		"sub":    "user123",
		"iss":    "https://issuer.example.com",
		"aud":    "my-app",
		"tenant": "acme",
	}
	boundClaims := map[string]any{
		"iss":    "https://issuer.example.com",
		"tenant": "acme",
	}

	err := validateBoundClaims(claims, boundClaims)
	assert.NoError(t, err)
}

func TestValidateBoundClaims_MissingClaim(t *testing.T) {
	claims := map[string]interface{}{
		"sub": "user123",
	}
	boundClaims := map[string]any{
		"tenant": "acme",
	}

	err := validateBoundClaims(claims, boundClaims)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestValidateBoundClaims_MismatchedValue(t *testing.T) {
	claims := map[string]interface{}{
		"sub":    "user123",
		"tenant": "other-tenant",
	}
	boundClaims := map[string]any{
		"tenant": "acme",
	}

	err := validateBoundClaims(claims, boundClaims)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "mismatch")
}

func TestValidateBoundClaims_NumericClaim(t *testing.T) {
	claims := map[string]interface{}{
		"sub":   "user123",
		"level": float64(5), // JSON numbers decode as float64
	}
	boundClaims := map[string]any{
		"level": float64(5),
	}

	err := validateBoundClaims(claims, boundClaims)
	assert.NoError(t, err)
}

func TestValidateBoundClaims_BooleanClaim(t *testing.T) {
	claims := map[string]interface{}{
		"sub":      "user123",
		"is_admin": true,
	}
	boundClaims := map[string]any{
		"is_admin": true,
	}

	err := validateBoundClaims(claims, boundClaims)
	assert.NoError(t, err)
}

// =============================================================================
// claimValuesEqual Type Safety Tests (Phase 4: M1)
// =============================================================================

func TestClaimValuesEqual_TypeSafety(t *testing.T) {
	tests := []struct {
		name     string
		actual   interface{}
		expected interface{}
		want     bool
	}{
		// Same-type matches
		{"string match", "hello", "hello", true},
		{"string mismatch", "hello", "world", false},
		{"float64 match", float64(42), float64(42), true},
		{"float64 mismatch", float64(42), float64(99), false},
		{"int match", 5, 5, true},
		{"int mismatch", 5, 6, false},
		{"bool true match", true, true, true},
		{"bool false match", false, false, true},
		{"bool mismatch", true, false, false},

		// Cross-type numeric (allowed: JSON numbers are float64)
		{"float64 actual vs int expected", float64(5), 5, true},
		{"int actual vs float64 expected", 5, float64(5), true},
		{"int64 actual vs float64 expected", int64(5), float64(5), true},

		// Cross-type rejection (no coercion between unrelated types)
		{"int vs string rejected", 1, "1", false},
		{"string vs int rejected", "1", 1, false},
		{"bool vs string rejected", true, "true", false},
		{"string vs bool rejected", "true", true, false},
		{"float vs string rejected", float64(1.0), "1", false},
		{"string vs float rejected", "1", float64(1.0), false},
		{"bool vs int rejected", true, 1, false},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := claimValuesEqual(tc.actual, tc.expected)
			assert.Equal(t, tc.want, got, "claimValuesEqual(%v [%T], %v [%T])", tc.actual, tc.actual, tc.expected, tc.expected)
		})
	}
}

// =============================================================================
// extractClaim Tests
// =============================================================================

func TestExtractClaim_StringValue(t *testing.T) {
	claims := map[string]interface{}{
		"sub":   "user123",
		"email": "user@example.com",
	}

	result := extractClaim(claims, "sub")
	assert.Equal(t, "user123", result)

	result = extractClaim(claims, "email")
	assert.Equal(t, "user@example.com", result)
}

func TestExtractClaim_MissingClaim(t *testing.T) {
	claims := map[string]interface{}{
		"sub": "user123",
	}

	result := extractClaim(claims, "email")
	assert.Empty(t, result)
}

func TestExtractClaim_NumericValue(t *testing.T) {
	claims := map[string]interface{}{
		"level": 42,
	}

	result := extractClaim(claims, "level")
	assert.Equal(t, "42", result)
}

func TestExtractClaim_FloatValue(t *testing.T) {
	claims := map[string]interface{}{
		"score": 99.5,
	}

	result := extractClaim(claims, "score")
	assert.Equal(t, "99.5", result)
}

func TestExtractClaim_BoolValue(t *testing.T) {
	claims := map[string]interface{}{
		"verified": true,
	}

	result := extractClaim(claims, "verified")
	assert.Equal(t, "true", result)
}

func TestExtractClaim_ArrayValue(t *testing.T) {
	claims := map[string]interface{}{
		"groups": []interface{}{"admin", "users", "developers"},
	}

	result := extractClaim(claims, "groups")
	assert.Equal(t, "admin,users,developers", result)
}

func TestExtractClaim_EmptyArray(t *testing.T) {
	claims := map[string]interface{}{
		"groups": []interface{}{},
	}

	result := extractClaim(claims, "groups")
	assert.Empty(t, result)
}

func TestExtractClaim_MixedArray(t *testing.T) {
	claims := map[string]interface{}{
		"mixed": []interface{}{"string", 42, true},
	}

	result := extractClaim(claims, "mixed")
	assert.Equal(t, "string,42,true", result)
}

func TestExtractClaim_NestedPath(t *testing.T) {
	// Note: current implementation doesn't support nested paths
	// This test documents current behavior
	claims := map[string]interface{}{
		"user": map[string]interface{}{
			"name": "John",
		},
	}

	// Direct access doesn't work for nested
	result := extractClaim(claims, "user.name")
	assert.Empty(t, result)

	// But the whole nested object can be extracted
	result = extractClaim(claims, "user")
	assert.Contains(t, result, "name")
}

// =============================================================================
// parseNumericClaim Tests
// =============================================================================

func TestParseNumericClaim(t *testing.T) {
	tests := []struct {
		name    string
		value   any
		wantVal int64
		wantOk  bool
	}{
		{"float64", float64(1234567890), 1234567890, true},
		{"int64", int64(1234567890), 1234567890, true},
		{"int", int(1234567890), 1234567890, true},
		{"float64 with fraction (truncates)", float64(1234567890.5), 1234567890, true},
		{"string rejected", "1234567890", 0, false},
		{"bool rejected", true, 0, false},
		{"nil rejected", nil, 0, false},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			val, ok := parseNumericClaim(tc.value)
			assert.Equal(t, tc.wantOk, ok)
			if ok {
				assert.Equal(t, tc.wantVal, val)
			}
		})
	}
}

// =============================================================================
// Table-Driven Tests
// =============================================================================

func TestValidateBoundClaims_TableDriven(t *testing.T) {
	tests := []struct {
		name        string
		claims      map[string]interface{}
		boundClaims map[string]any
		expectError bool
		errorMsg    string
	}{
		{
			name:        "No bound claims",
			claims:      map[string]interface{}{"sub": "user"},
			boundClaims: nil,
			expectError: false,
		},
		{
			name:        "Single matching claim",
			claims:      map[string]interface{}{"sub": "user", "iss": "issuer"},
			boundClaims: map[string]any{"iss": "issuer"},
			expectError: false,
		},
		{
			name:        "Missing required claim",
			claims:      map[string]interface{}{"sub": "user"},
			boundClaims: map[string]any{"iss": "issuer"},
			expectError: true,
			errorMsg:    "not found",
		},
		{
			name:        "Mismatched claim value",
			claims:      map[string]interface{}{"sub": "user", "iss": "wrong"},
			boundClaims: map[string]any{"iss": "expected"},
			expectError: true,
			errorMsg:    "mismatch",
		},
		{
			name:        "Multiple matching claims",
			claims:      map[string]interface{}{"a": "1", "b": "2", "c": "3"},
			boundClaims: map[string]any{"a": "1", "c": "3"},
			expectError: false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := validateBoundClaims(tc.claims, tc.boundClaims)

			if tc.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.errorMsg)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

// =============================================================================
// validateBoundURIPatterns Tests (via helper.MatchAny)
// =============================================================================

func TestBoundURIPatterns_Validation(t *testing.T) {
	tests := []struct {
		name       string
		claimValue string
		patterns   []string
		wantMatch  bool
	}{
		// SPIFFE ID patterns
		{"exact spiffe match", "spiffe://example.com/dept/svc", []string{"spiffe://example.com/dept/svc"}, true},
		{"plus trust domain", "spiffe://example.com/dept/svc", []string{"spiffe://+/dept/svc"}, true},
		{"prefix wildcard", "spiffe://example.com/dept/team/svc", []string{"spiffe://example.com/dept/*"}, true},
		{"scheme catch-all", "spiffe://anything/any/path", []string{"spiffe://*"}, true},
		{"combined plus and star", "spiffe://example.com/dept/team/svc", []string{"spiffe://+/dept/*"}, true},
		{"mismatch domain", "spiffe://other.com/dept/svc", []string{"spiffe://example.com/dept/svc"}, false},
		{"mismatch path", "spiffe://example.com/other/svc", []string{"spiffe://+/dept/svc"}, false},
		{"wrong scheme", "https://example.com/path", []string{"spiffe://*"}, false},

		// Multiple patterns (OR semantics)
		{"matches second pattern", "spiffe://other.com/web", []string{"spiffe://example.com/web", "spiffe://other.com/web"}, true},
		{"matches none", "spiffe://third.com/web", []string{"spiffe://example.com/web", "spiffe://other.com/web"}, false},

		// Empty claim
		{"empty claim value", "", []string{"spiffe://*"}, false},
		{"empty patterns", "spiffe://example.com/svc", []string{}, false},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			claims := map[string]interface{}{
				"sub": tc.claimValue,
			}

			claimValue := extractClaim(claims, "sub")
			var matched bool
			if claimValue != "" && len(tc.patterns) > 0 {
				matched = helper.MatchAny(claimValue, tc.patterns)
			}

			if tc.wantMatch {
				assert.True(t, matched, "expected claim %q to match patterns %v", tc.claimValue, tc.patterns)
			} else {
				assert.False(t, matched, "expected claim %q NOT to match patterns %v", tc.claimValue, tc.patterns)
			}
		})
	}
}

func TestExtractClaim_TableDriven(t *testing.T) {
	tests := []struct {
		name      string
		claims    map[string]interface{}
		claimName string
		expected  string
	}{
		{
			name:      "String claim",
			claims:    map[string]interface{}{"sub": "user123"},
			claimName: "sub",
			expected:  "user123",
		},
		{
			name:      "Missing claim",
			claims:    map[string]interface{}{"sub": "user123"},
			claimName: "email",
			expected:  "",
		},
		{
			name:      "Integer claim",
			claims:    map[string]interface{}{"level": 5},
			claimName: "level",
			expected:  "5",
		},
		{
			name:      "Boolean true",
			claims:    map[string]interface{}{"active": true},
			claimName: "active",
			expected:  "true",
		},
		{
			name:      "Boolean false",
			claims:    map[string]interface{}{"active": false},
			claimName: "active",
			expected:  "false",
		},
		{
			name:      "Array claim",
			claims:    map[string]interface{}{"roles": []interface{}{"a", "b"}},
			claimName: "roles",
			expected:  "a,b",
		},
		{
			name:      "Empty string",
			claims:    map[string]interface{}{"name": ""},
			claimName: "name",
			expected:  "",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := extractClaim(tc.claims, tc.claimName)
			assert.Equal(t, tc.expected, result)
		})
	}
}

func TestExtractGroupsClaim(t *testing.T) {
	tests := []struct {
		name     string
		claims   map[string]interface{}
		claim    string
		expected []string
	}{
		{
			name:     "string array",
			claims:   map[string]interface{}{"groups": []interface{}{"db-read", "monitoring"}},
			claim:    "groups",
			expected: []string{"db-read", "monitoring"},
		},
		{
			name:     "single string",
			claims:   map[string]interface{}{"role": "admin"},
			claim:    "role",
			expected: []string{"admin"},
		},
		{
			name:     "comma-separated string",
			claims:   map[string]interface{}{"groups": "db-read,monitoring, admin"},
			claim:    "groups",
			expected: []string{"db-read", "monitoring", "admin"},
		},
		{
			name:     "missing claim",
			claims:   map[string]interface{}{"sub": "user1"},
			claim:    "groups",
			expected: nil,
		},
		{
			name:     "empty string",
			claims:   map[string]interface{}{"groups": ""},
			claim:    "groups",
			expected: nil,
		},
		{
			name:     "empty array",
			claims:   map[string]interface{}{"groups": []interface{}{}},
			claim:    "groups",
			expected: []string{},
		},
		{
			name:     "array with non-string elements skipped",
			claims:   map[string]interface{}{"groups": []interface{}{"admin", 42, "reader"}},
			claim:    "groups",
			expected: []string{"admin", "reader"},
		},
		{
			name:     "non-string non-array type returns nil",
			claims:   map[string]interface{}{"groups": 42},
			claim:    "groups",
			expected: nil,
		},
		{
			name:     "cognito:groups nested claim name",
			claims:   map[string]interface{}{"cognito:groups": []interface{}{"db-read"}},
			claim:    "cognito:groups",
			expected: []string{"db-read"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := extractGroupsClaim(tc.claims, tc.claim)
			assert.Equal(t, tc.expected, result)
		})
	}
}

func TestVerifyURLReachable_Success(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer srv.Close()

	err := verifyURLReachable(context.Background(), srv.URL, "")
	assert.NoError(t, err)
}

func TestVerifyURLReachable_NonOKStatus(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()

	err := verifyURLReachable(context.Background(), srv.URL, "")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "unexpected status code")
}

func TestVerifyURLReachable_Unreachable(t *testing.T) {
	err := verifyURLReachable(context.Background(), "http://localhost:1", "")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to connect")
}

func TestVerifyURLReachable_InvalidCA(t *testing.T) {
	err := verifyURLReachable(context.Background(), "https://example.com", "not-a-pem")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to parse CA certificate")
}

// =============================================================================
// verifyOIDCDiscoveryURLReachable Tests
// =============================================================================

func TestVerifyOIDCDiscoveryURLReachable(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/.well-known/openid-configuration" {
			w.WriteHeader(http.StatusOK)
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer srv.Close()

	err := verifyOIDCDiscoveryURLReachable(context.Background(), srv.URL, "")
	assert.NoError(t, err)

	// With trailing slash
	err = verifyOIDCDiscoveryURLReachable(context.Background(), srv.URL+"/", "")
	assert.NoError(t, err)
}

// =============================================================================
// extractActChain Tests (RFC 8693 §4.1 "act" claim)
// =============================================================================

func TestExtractActChain_NoActClaim(t *testing.T) {
	claims := map[string]interface{}{"sub": "mcp-github"}
	assert.Nil(t, extractActChain(claims))
}

func TestExtractActChain_SingleHop(t *testing.T) {
	claims := map[string]interface{}{
		"sub": "mcp-github",
		"act": map[string]interface{}{"sub": "agents/alpha"},
	}
	got := extractActChain(claims)
	require.Len(t, got, 1)
	assert.Equal(t, "agents/alpha", got[0].Subject)
	assert.True(t, got[0].Verified)
}

func TestExtractActChain_NestedChain(t *testing.T) {
	claims := map[string]interface{}{
		"sub": "mcp-github",
		"act": map[string]interface{}{
			"sub": "broker-beta",
			"act": map[string]interface{}{
				"sub": "agents/alpha",
			},
		},
	}
	got := extractActChain(claims)
	require.Len(t, got, 2)
	assert.Equal(t, "broker-beta", got[0].Subject)
	assert.Equal(t, "agents/alpha", got[1].Subject)
	for _, a := range got {
		assert.True(t, a.Verified, "all act-claim actors are verified")
	}
}

func TestExtractActChain_NonObjectAct(t *testing.T) {
	// "act" must be a JSON object; a string here is malformed and ends the walk.
	claims := map[string]interface{}{
		"sub": "mcp-github",
		"act": "agents/alpha",
	}
	assert.Empty(t, extractActChain(claims))
}

func TestExtractActChain_MissingSubInLayer(t *testing.T) {
	// Inner layer has no sub → walk terminates without recording.
	claims := map[string]interface{}{
		"sub": "mcp-github",
		"act": map[string]interface{}{
			"act": map[string]interface{}{"sub": "agents/alpha"},
		},
	}
	assert.Empty(t, extractActChain(claims))
}

func TestExtractActChain_NonStringSub(t *testing.T) {
	// sub must be a string; a number here is malformed.
	claims := map[string]interface{}{
		"sub": "mcp-github",
		"act": map[string]interface{}{"sub": 42},
	}
	assert.Empty(t, extractActChain(claims))
}

func TestExtractActChain_EmptyStringSub(t *testing.T) {
	claims := map[string]interface{}{
		"sub": "mcp-github",
		"act": map[string]interface{}{"sub": ""},
	}
	assert.Empty(t, extractActChain(claims))
}

func TestExtractActChain_DepthCap(t *testing.T) {
	// Build a chain deeper than maxActChainDepth (4). The walk should stop
	// after 4 layers, retaining the layers CLOSEST to the principal (not
	// the innermost ones) — i.e. the first 4 entries when walking outward.
	innermost := map[string]interface{}{"sub": "layer-6"}
	chain := innermost
	for i := 5; i >= 1; i-- {
		chain = map[string]interface{}{
			"sub": strconv.Itoa(i),
			"act": chain,
		}
	}
	// Re-assign subs as "layer-N" once nesting is built, so we can assert order.
	walk := chain
	for i := 1; i <= 5; i++ {
		walk["sub"] = "layer-" + strconv.Itoa(i)
		next, ok := walk["act"].(map[string]interface{})
		if !ok {
			break
		}
		walk = next
	}
	claims := map[string]interface{}{"sub": "principal", "act": chain}

	got := extractActChain(claims)
	require.Len(t, got, maxActChainDepth)
	assert.Equal(t, "layer-1", got[0].Subject, "outermost act layer must come first")
	assert.Equal(t, "layer-2", got[1].Subject)
	assert.Equal(t, "layer-3", got[2].Subject)
	assert.Equal(t, "layer-4", got[3].Subject)
}
