package middleware_test

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

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

	"github.com/veylant/ia-gateway/internal/middleware"
)

// buildToken encodes claims as a JSON string — used with JSONMockVerifier in auth tests.
func buildToken(claims middleware.UserClaims) string {
	b, _ := json.Marshal(claims)
	return string(b)
}

// okHandler always returns HTTP 200.
var okHandler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
	w.WriteHeader(http.StatusOK)
})

func TestAuth_ValidToken_InjectsClaimsAndCallsNext(t *testing.T) {
	expected := &middleware.UserClaims{
		UserID:   "user-123",
		TenantID: "tenant-456",
		Email:    "user@veylant.dev",
		Roles:    []string{"user"},
	}
	verifier := &middleware.MockVerifier{Claims: expected}

	var got *middleware.UserClaims
	next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
		got, _ = middleware.ClaimsFromContext(r.Context())
	})

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
	req.Header.Set("Authorization", "Bearer somevalidtoken")
	rec := httptest.NewRecorder()

	middleware.Auth(verifier)(next).ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code)
	require.NotNil(t, got)
	assert.Equal(t, expected.UserID, got.UserID)
	assert.Equal(t, expected.TenantID, got.TenantID)
	assert.Equal(t, expected.Roles, got.Roles)
}

func TestAuth_MissingAuthorizationHeader_Returns401(t *testing.T) {
	verifier := &middleware.MockVerifier{}

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
	rec := httptest.NewRecorder()

	middleware.Auth(verifier)(okHandler).ServeHTTP(rec, req)

	assert.Equal(t, http.StatusUnauthorized, rec.Code)
	assertOpenAIError(t, rec, "authentication_error")
}

func TestAuth_ExpiredToken_Returns401(t *testing.T) {
	verifier := &middleware.MockVerifier{Err: errors.New("token is expired")}

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
	req.Header.Set("Authorization", "Bearer expiredtoken")
	rec := httptest.NewRecorder()

	middleware.Auth(verifier)(okHandler).ServeHTTP(rec, req)

	assert.Equal(t, http.StatusUnauthorized, rec.Code)
	assertOpenAIError(t, rec, "authentication_error")
}

func TestAuth_BadIssuerToken_Returns401(t *testing.T) {
	verifier := &middleware.MockVerifier{Err: errors.New("oidc: issuer mismatch")}

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
	req.Header.Set("Authorization", "Bearer badissuertoken")
	rec := httptest.NewRecorder()

	middleware.Auth(verifier)(okHandler).ServeHTTP(rec, req)

	assert.Equal(t, http.StatusUnauthorized, rec.Code)
	assertOpenAIError(t, rec, "authentication_error")
}

func TestAuth_ValidToken_ClaimsFullyExtracted(t *testing.T) {
	// Use JSONMockVerifier to prove full claim extraction pipeline.
	expected := middleware.UserClaims{
		UserID:   "00000000-0000-0000-0000-000000000099",
		TenantID: "00000000-0000-0000-0000-000000000001",
		Email:    "admin@veylant.dev",
		Roles:    []string{"admin", "user"},
	}

	var got *middleware.UserClaims
	next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
		got, _ = middleware.ClaimsFromContext(r.Context())
	})

	req := httptest.NewRequest(http.MethodPost, "/", nil)
	req.Header.Set("Authorization", "Bearer "+buildToken(expected))
	rec := httptest.NewRecorder()

	verifier := &middleware.MockVerifier{
		Claims: &expected,
	}
	middleware.Auth(verifier)(next).ServeHTTP(rec, req)

	require.NotNil(t, got)
	assert.Equal(t, expected.UserID, got.UserID)
	assert.Equal(t, expected.TenantID, got.TenantID)
	assert.Equal(t, expected.Email, got.Email)
	assert.ElementsMatch(t, expected.Roles, got.Roles)
}

func TestAuth_MalformedBearerScheme_Returns401(t *testing.T) {
	verifier := &middleware.MockVerifier{}

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
	rec := httptest.NewRecorder()

	middleware.Auth(verifier)(okHandler).ServeHTTP(rec, req)

	assert.Equal(t, http.StatusUnauthorized, rec.Code)
}

// assertOpenAIError checks that the response body has the OpenAI error envelope
// with the given error type.
func assertOpenAIError(t *testing.T, rec *httptest.ResponseRecorder, expectedType string) {
	t.Helper()
	var body struct {
		Error struct {
			Type string `json:"type"`
		} `json:"error"`
	}
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&body))
	assert.Equal(t, expectedType, body.Error.Type)
}

// Compile-time check that MockVerifier satisfies TokenVerifier.
var _ middleware.TokenVerifier = (*middleware.MockVerifier)(nil)

// Compile-time check that context accessors are available from external packages.
func TestContextAccessors(t *testing.T) {
	ctx := context.Background()

	// No claims set — should return false.
	_, ok := middleware.ClaimsFromContext(ctx)
	assert.False(t, ok)

	// No request ID set — should return empty string.
	assert.Empty(t, middleware.RequestIDFromContext(ctx))
}
