package regression

import (
	"net/http"
	"net/http/httptest"
	"testing"

	traefikoidc "github.com/lukaszraczylo/traefikoidc"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestIssueRegressions consolidates regression tests for reported GitHub issues
func TestIssueRegressions(t *testing.T) {
	t.Run("Issue53_CSRF_Missing_In_Session", testIssue53CSRFRegression)
	t.Run("Issue53_Reverse_Proxy_HTTPS_Detection", testIssue53ReverseProxyHTTPS)
	t.Run("Issue53_SameSite_Cookie_Handling", testIssue53SameSiteCookies)
	t.Run("Issue60_Missing_Claim_Fields", testIssue60MissingClaimFields)
	t.Run("Issue60_Safe_Template_Functions", testIssue60SafeTemplateFunctions)
	t.Run("Issue60_Double_Processing_Concern", testIssue60DoubleProcessing)
}

// testIssue53CSRFRegression tests the specific issue reported in GitHub issue #53
// where Azure OIDC authentication fails with "CSRF token missing in session"
// This was caused by incorrect HTTPS detection in reverse proxy environments
func testIssue53CSRFRegression(t *testing.T) {
	// This test reproduces the exact scenario from issue #53:
	// 1. User accesses app via HTTPS through Traefik
	// 2. Traefik terminates SSL and forwards HTTP internally
	// 3. Session cookies must be properly configured for HTTPS
	// 4. CSRF token must persist through the OAuth flow

	sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", "", 0, traefikoidc.NewLogger("debug"))
	require.NoError(t, err)

	// Step 1: Initial request to protected resource
	// User accesses https://app.example.com/protected
	// Traefik forwards as http://internal/protected with X-Forwarded-Proto: https
	initReq := httptest.NewRequest("GET", "http://internal/protected", nil)
	initReq.Header.Set("X-Forwarded-Proto", "https")
	initReq.Header.Set("X-Forwarded-Host", "app.example.com")
	initReq.Header.Set("User-Agent", "Mozilla/5.0") // Real browser

	// Get session and set OAuth flow data
	session, err := sessionManager.GetSession(initReq)
	require.NoError(t, err)

	// Set CSRF and other OAuth data
	csrfToken := "csrf-token-for-azure"
	nonce := "nonce-for-azure"
	session.SetCSRF(csrfToken)
	session.SetNonce(nonce)
	session.SetCodeVerifier("pkce-verifier")
	session.SetIncomingPath("/protected")
	session.MarkDirty()

	// Save session - this is where the bug was
	// Previously: used r.URL.Scheme which is always "http" behind proxy
	// Now: uses X-Forwarded-Proto header
	rec := httptest.NewRecorder()
	err = session.Save(initReq, rec)
	require.NoError(t, err)

	// Verify cookies are secure
	cookies := rec.Result().Cookies()
	require.NotEmpty(t, cookies, "Cookies must be set")

	var mainCookie *http.Cookie
	for _, cookie := range cookies {
		if cookie.Name == "_oidc_raczylo_m" {
			mainCookie = cookie
			break
		}
	}
	require.NotNil(t, mainCookie, "Main session cookie must be set")

	// Critical assertions for issue #53
	assert.True(t, mainCookie.Secure, "Cookie MUST have Secure flag for HTTPS (was the bug)")
	assert.Equal(t, http.SameSiteLaxMode, mainCookie.SameSite, "MUST use Lax for OAuth callbacks to work")
	assert.Equal(t, "/", mainCookie.Path, "Cookie path must be root")
	assert.True(t, mainCookie.HttpOnly, "Cookie must be HttpOnly")
	assert.Equal(t, "app.example.com", mainCookie.Domain, "Domain should use X-Forwarded-Host")

	// Step 2: OAuth provider redirects back to callback
	// Azure redirects to https://app.example.com/oidc/callback?code=...&state=...
	// Traefik forwards as http://internal/oidc/callback with headers
	callbackReq := httptest.NewRequest("GET",
		"http://internal/oidc/callback?code=azure-auth-code&state="+csrfToken, nil)
	callbackReq.Header.Set("X-Forwarded-Proto", "https")
	callbackReq.Header.Set("X-Forwarded-Host", "app.example.com")
	callbackReq.Header.Set("User-Agent", "Mozilla/5.0")

	// Add cookies from initial request
	// Browser sends secure cookies because request is HTTPS
	for _, cookie := range cookies {
		callbackReq.AddCookie(cookie)
	}

	// Get session in callback
	callbackSession, err := sessionManager.GetSession(callbackReq)
	require.NoError(t, err)

	// Verify CSRF token is present (was missing in issue #53)
	retrievedCSRF := callbackSession.GetCSRF()
	assert.Equal(t, csrfToken, retrievedCSRF,
		"CSRF token MUST persist (was missing in issue #53)")

	// Verify other session data also persists
	assert.Equal(t, nonce, callbackSession.GetNonce(),
		"Nonce must persist for security")
	assert.Equal(t, "pkce-verifier", callbackSession.GetCodeVerifier(),
		"PKCE verifier must persist")
	assert.Equal(t, "/protected", callbackSession.GetIncomingPath(),
		"Original path must persist for redirect after auth")
}

// testIssue53ReverseProxyHTTPS tests HTTPS detection in reverse proxy setups
func testIssue53ReverseProxyHTTPS(t *testing.T) {
	sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", "", 0, traefikoidc.NewLogger("debug"))
	require.NoError(t, err)

	// Create authenticated session with Azure tokens
	req := httptest.NewRequest("GET", "http://internal/api/data", nil)
	req.Header.Set("X-Forwarded-Proto", "https")
	req.Header.Set("X-Forwarded-Host", "app.example.com")

	session, err := sessionManager.GetSession(req)
	require.NoError(t, err)

	// Simulate successful Azure authentication
	session.SetAuthenticated(true)
	session.SetUserIdentifier("user@example.com")
	// Azure may use opaque access tokens
	session.SetAccessToken("opaque-azure-access-token")
	session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ") // trufflehog:ignore
	session.SetRefreshToken("azure-refresh-token")

	// Save with proper security
	rec := httptest.NewRecorder()
	err = session.Save(req, rec)
	require.NoError(t, err)

	// Verify session can be retrieved and tokens are intact
	cookies := rec.Result().Cookies()
	req2 := httptest.NewRequest("GET", "http://internal/api/data", nil)
	req2.Header.Set("X-Forwarded-Proto", "https")
	for _, cookie := range cookies {
		req2.AddCookie(cookie)
	}

	session2, err := sessionManager.GetSession(req2)
	require.NoError(t, err)

	assert.True(t, session2.GetAuthenticated(), "User should remain authenticated")
	assert.Equal(t, "user@example.com", session2.GetUserIdentifier())
	assert.NotEmpty(t, session2.GetAccessToken(), "Access token should persist")
	assert.NotEmpty(t, session2.GetIDToken(), "ID token should persist")
	assert.NotEmpty(t, session2.GetRefreshToken(), "Refresh token should persist")

	// Test redirect loop prevention
	for i := 0; i < 3; i++ {
		session2.IncrementRedirectCount()
	}

	// Verify redirect count is tracked
	count := session2.GetRedirectCount()
	assert.Equal(t, 3, count, "Redirect count should be tracked")

	// After successful auth, count should be reset
	session2.SetAuthenticated(true)
	session2.ResetRedirectCount()
	assert.Equal(t, 0, session2.GetRedirectCount(), "Count should reset after auth")
}

// testIssue53SameSiteCookies tests SameSite cookie attribute handling
// in different reverse proxy scenarios
func testIssue53SameSiteCookies(t *testing.T) {
	testCases := []struct {
		name             string
		proto            string
		description      string
		expectedSameSite http.SameSite
		expectedSecure   bool
	}{
		{
			name:             "HTTPS via proxy",
			proto:            "https",
			expectedSecure:   true,
			expectedSameSite: http.SameSiteLaxMode,
			description:      "HTTPS should use Lax SameSite for OAuth callbacks",
		},
		{
			name:             "HTTP direct",
			proto:            "",
			expectedSecure:   false,
			expectedSameSite: http.SameSiteLaxMode,
			description:      "HTTP should use Lax SameSite for compatibility",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", "", 0, traefikoidc.NewLogger("debug"))
			require.NoError(t, err)

			req := httptest.NewRequest("GET", "http://internal/test", nil)
			if tc.proto != "" {
				req.Header.Set("X-Forwarded-Proto", tc.proto)
			}
			req.Header.Set("User-Agent", "Mozilla/5.0")

			session, err := sessionManager.GetSession(req)
			require.NoError(t, err)
			session.SetCSRF("test")

			rec := httptest.NewRecorder()
			err = session.Save(req, rec)
			require.NoError(t, err)

			cookies := rec.Result().Cookies()
			for _, cookie := range cookies {
				if cookie.Name == "_oidc_raczylo_m" {
					assert.Equal(t, tc.expectedSecure, cookie.Secure, tc.description)
					assert.Equal(t, tc.expectedSameSite, cookie.SameSite, tc.description)
					break
				}
			}
		})
	}
}

// testIssue60MissingClaimFields tests handling of missing claim fields (GitHub issue #60)
func testIssue60MissingClaimFields(t *testing.T) {
	config := traefikoidc.CreateConfig()
	config.ProviderURL = "https://example.com"
	config.ClientID = "test-client"
	config.ClientSecret = "test-secret"
	config.CallbackURL = "/callback"
	config.SessionEncryptionKey = "test-encryption-key-32-characters"

	testCases := []struct {
		name           string
		description    string
		headers        []traefikoidc.TemplatedHeader
		shouldValidate bool
	}{
		{
			name: "Direct claim access",
			headers: []traefikoidc.TemplatedHeader{
				{Name: "X-User-Email", Value: "{{.Claims.email}}"},
				{Name: "X-Internal-Role", Value: "{{.Claims.internal_role}}"},
			},
			shouldValidate: true,
			description:    "Direct claim access should validate",
		},
		{
			name: "Azure AD claims",
			headers: []traefikoidc.TemplatedHeader{
				{Name: "X-User-Email", Value: "{{.Claims.email}}"},
				{Name: "X-User-OID", Value: "{{.Claims.oid}}"},
				{Name: "X-User-TID", Value: "{{.Claims.tid}}"},
				{Name: "X-User-UPN", Value: "{{.Claims.upn}}"},
				{Name: "X-Internal-Role", Value: "{{.Claims.internal_role}}"}, // Custom claim from issue #60
			},
			shouldValidate: true,
			description:    "Azure AD claims should validate",
		},
		{
			name: "Valid context fields",
			headers: []traefikoidc.TemplatedHeader{
				{Name: "X-Access-Token", Value: "{{.AccessToken}}"},
				{Name: "X-ID-Token", Value: "{{.IdToken}}"},
				{Name: "X-Refresh-Token", Value: "{{.RefreshToken}}"},
				{Name: "X-User-Email", Value: "{{.Claims.email}}"},
				{Name: "X-User-Sub", Value: "{{.Claims.sub}}"},
			},
			shouldValidate: true,
			description:    "All valid context fields should pass validation",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			config.Headers = tc.headers
			err := config.Validate()
			if tc.shouldValidate {
				assert.NoError(t, err, tc.description)
			} else {
				assert.Error(t, err, tc.description)
			}
		})
	}
}

// testIssue60SafeTemplateFunctions tests safe template functions for handling missing fields
func testIssue60SafeTemplateFunctions(t *testing.T) {
	config := traefikoidc.CreateConfig()
	config.ProviderURL = "https://example.com"
	config.ClientID = "test-client"
	config.ClientSecret = "test-secret"
	config.CallbackURL = "/callback"
	config.SessionEncryptionKey = "test-encryption-key-32-characters"

	// Templates using safe functions for missing fields
	config.Headers = []traefikoidc.TemplatedHeader{
		{Name: "X-User-Email", Value: "{{.Claims.email}}"},
		{Name: "X-User-Role", Value: "{{get .Claims \"internal_role\"}}"},
		{Name: "X-User-Dept", Value: "{{default \"unknown\" .Claims.department}}"},
		{Name: "X-User-Groups", Value: "{{with .Claims.groups}}{{.}}{{end}}"},
	}

	// Configuration should validate successfully
	err := config.Validate()
	assert.NoError(t, err, "Config with safe template functions should validate")

	// Test that dangerous templates are rejected
	dangerousTemplates := []traefikoidc.TemplatedHeader{
		{Name: "X-Bad-1", Value: "{{call .SomeFunc}}"},
		{Name: "X-Bad-2", Value: "{{range .Items}}{{.}}{{end}}"},
		{Name: "X-Bad-3", Value: "{{index .Array 0}}"},
		{Name: "X-Bad-4", Value: "{{printf \"%s\" .Data}}"},
	}

	for _, header := range dangerousTemplates {
		config.Headers = []traefikoidc.TemplatedHeader{header}
		err := config.Validate()
		require.Error(t, err, "Dangerous template should be rejected: %s", header.Value)
		assert.Contains(t, err.Error(), "dangerous", "Error should mention dangerous pattern")
	}

	// Test all safe patterns from the documentation
	safePatterns := []traefikoidc.TemplatedHeader{
		// Basic field access
		{Name: "X-User-Role", Value: "{{.Claims.internal_role}}"},
		// Using the get function
		{Name: "X-User-Role-Get", Value: "{{get .Claims \"internal_role\"}}"},
		// Using the default function
		{Name: "X-User-Role-Default", Value: "{{default \"guest\" .Claims.role}}"},
		// Nested fields with 'with'
		{Name: "X-User-Admin", Value: "{{with .Claims.groups}}{{.admin}}{{end}}"},
	}

	config.Headers = safePatterns
	err = config.Validate()
	assert.NoError(t, err, "All safe patterns from guide should validate")
}

// testIssue60DoubleProcessing tests the user's concern about double processing of templates
func testIssue60DoubleProcessing(t *testing.T) {
	// The user was concerned that templates might be processed twice:
	// 1. Once when Traefik parses the config
	// 2. Once when the plugin executes the template

	// This test verifies that templates are stored as strings during config parsing
	config := &traefikoidc.Config{
		Headers: []traefikoidc.TemplatedHeader{
			{Name: "X-Test", Value: "{{.Claims.test}}"},
		},
	}

	// The template should still be a raw string after config creation
	assert.Equal(t, "{{.Claims.test}}", config.Headers[0].Value,
		"Template should remain as raw string in config")

	// Test that our custom function syntax survives config marshaling/unmarshaling
	originalValue := `{{get .Claims "internal_role"}}`
	header := traefikoidc.TemplatedHeader{
		Name:  "X-Role",
		Value: originalValue,
	}

	// Even after any marshaling/unmarshaling, the template string should be preserved
	assert.Equal(t, originalValue, header.Value,
		"Template with functions should be preserved exactly")
}
