package middleware

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

	"github.com/pingcap/agent-git-service/internal/crypto"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/embedding"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/tenant"
	"github.com/pingcap/agent-git-service/internal/testharness/testdb"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
	"gorm.io/gorm"
)

func base64Encode(s string) string {
	return base64.StdEncoding.EncodeToString([]byte(s))
}

// okHandler is a simple handler that writes 200 OK.
var okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("ok"))
})

type authTestTenantProvider struct {
	resources *service.TenantResources
}

func (p authTestTenantProvider) ForTenant(id tenant.ID) (*service.TenantResources, error) {
	if p.resources == nil || id != p.resources.Info.ID {
		return nil, service.ErrTenantResourcesUnavailable
	}
	return p.resources, nil
}

type authTestFixture struct {
	parent    context.Context
	info      tenant.Info
	resources *service.TenantResources
	svc       *service.Service
}

func setupTestService(t *testing.T, allowAnyToken bool) *authTestFixture {
	t.Helper()
	info := tenant.Info{
		ID:               "middleware-test",
		CanonicalBaseURL: "http://example.com",
	}
	tenantCtx := tenant.WithInfo(t.Context(), info)
	gdb, cleanup := testdb.OpenRaw(t, "middleware")
	t.Cleanup(cleanup)
	if err := db.ClaimTenant(gdb.WithContext(tenantCtx), info.ID); err != nil {
		t.Fatalf("claim tenant database: %v", err)
	}
	if err := gdb.AutoMigrate(&db.User{}, &db.Token{}, &db.DeviceCode{}); err != nil {
		t.Fatalf("migrate auth tables: %v", err)
	}
	if err := db.EnforceTenantProvenance(gdb, info); err != nil {
		t.Fatalf("enforce tenant provenance: %v", err)
	}

	tmpDir, err := os.MkdirTemp("", "middleware-test-")
	if err != nil {
		t.Fatalf("create temp dir: %v", err)
	}
	t.Cleanup(func() { _ = os.RemoveAll(tmpDir) })
	git, err := gitstore.NewForTenant(tmpDir, info.ID)
	if err != nil {
		t.Fatalf("create tenant gitstore: %v", err)
	}
	wikiBlob := wikicatalog.NewBlobStore(tmpDir)
	wikiCatalog := wikicatalog.New(gdb, wikiBlob)
	secretBox, err := crypto.NewSecretBox("")
	if err != nil {
		t.Fatalf("create secret box: %v", err)
	}
	resources := &service.TenantResources{
		Info:          info,
		DB:            gdb,
		Git:           git,
		WikiCatalog:   wikiCatalog,
		WikiBlob:      wikiBlob,
		SecretBox:     secretBox,
		Embedder:      embedding.NopEmbedder{},
		AllowAnyToken: allowAnyToken,
	}
	if err := resources.Validate(); err != nil {
		t.Fatalf("validate tenant resources: %v", err)
	}
	svc, err := service.NewService(t.Context(), authTestTenantProvider{resources: resources})
	if err != nil {
		t.Fatalf("create service: %v", err)
	}
	t.Cleanup(func() { svc.Wg.Wait() })
	return &authTestFixture{
		parent:    t.Context(),
		info:      info,
		resources: resources,
		svc:       svc,
	}
}

func (f *authTestFixture) context() context.Context {
	return tenant.WithInfo(f.parent, f.info)
}

func (f *authTestFixture) db() *gorm.DB {
	return f.resources.DB.WithContext(f.context())
}

func (f *authTestFixture) request(method, target string) *http.Request {
	req := httptest.NewRequest(method, target, nil)
	return req.WithContext(tenant.WithInfo(req.Context(), f.info))
}

func seedUserWithToken(t *testing.T, fixture *authTestFixture, login string, token string) db.User {
	t.Helper()
	user := db.User{
		Login: login,
		Name:  login,
		Type:  db.TypeUser,
	}
	if err := fixture.db().Create(&user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if token != "" {
		if err := fixture.db().Create(&db.Token{UserID: user.ID, Value: token}).Error; err != nil {
			t.Fatalf("create token: %v", err)
		}
	}
	return user
}

func TestExtractToken(t *testing.T) {
	tests := []struct {
		name   string
		header string
		want   string
	}{
		{"empty", "", ""},
		{"token prefix", "token ghp_abc123", "ghp_abc123"},
		{"Token prefix mixed case", "Token ghp_abc123", "ghp_abc123"},
		{"bearer prefix", "bearer mytoken", "mytoken"},
		{"Bearer prefix", "Bearer mytoken", "mytoken"},
		{"basic auth extracts password", "Basic dXNlcjpwYXNz", "pass"},
		{"basic auth no colon", "Basic " + base64Encode("nopassword"), ""},
		{"basic auth empty password", "Basic " + base64Encode("user:"), ""},
		{"basic auth bad base64", "Basic !!!invalid!!!", ""},
		{"token with extra spaces", "  token   spaced  ", "spaced"},
		{"token only prefix", "token ", ""},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := httptest.NewRequest("GET", "/", nil)
			if tt.header != "" {
				r.Header.Set("Authorization", tt.header)
			}
			got := ExtractToken(r)
			if got != tt.want {
				t.Errorf("ExtractToken(%q) = %q, want %q", tt.header, got, tt.want)
			}
		})
	}
}

func TestTokenAuth_NoHeader(t *testing.T) {
	fixture := setupTestService(t, false)
	handler := TokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected 401, got %d", w.Code)
	}
	var body map[string]any
	if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
		t.Fatalf("failed to decode body: %v", err)
	}
	if body["message"] != "Requires authentication" {
		t.Errorf("unexpected message: %v", body["message"])
	}
}

func TestTokenAuth_NoTokensInDB_DefaultRejects(t *testing.T) {
	// Default (AllowAnyToken=false): empty token table rejects any token
	fixture := setupTestService(t, false)
	handler := TokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "token anyvalue")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected 401, got %d", w.Code)
	}
}

func TestTokenAuth_NoTokensInDB_AllowAnyToken(t *testing.T) {
	// AllowAnyToken=true: empty token table accepts any non-empty token
	fixture := setupTestService(t, true)
	if err := fixture.db().Create(&db.User{Login: "admin", Name: "admin", Type: db.TypeUser, SiteAdmin: true}).Error; err != nil {
		t.Fatalf("create admin: %v", err)
	}
	handler := TokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "token anyvalue")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusOK {
		t.Errorf("expected 200, got %d", w.Code)
	}
}

func TestTokenAuth_InvalidToken_WithTokensInDB(t *testing.T) {
	fixture := setupTestService(t, false)
	// Insert a real token so validation becomes strict
	seedUserWithToken(t, fixture, "valid-user", "valid-token")

	handler := TokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "token wrong-token")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected 401, got %d", w.Code)
	}
	var body map[string]any
	if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
		t.Fatalf("failed to decode body: %v", err)
	}
	if body["message"] != "Bad credentials" {
		t.Errorf("unexpected message: %v", body["message"])
	}
}

func TestTokenAuth_ValidToken_WithTokensInDB(t *testing.T) {
	fixture := setupTestService(t, false)
	seedUserWithToken(t, fixture, "valid-user", "valid-token")

	handler := TokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "token valid-token")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusOK {
		t.Errorf("expected 200, got %d", w.Code)
	}
}

func TestOptionalTokenAuth_NoHeader(t *testing.T) {
	fixture := setupTestService(t, false)
	handler := OptionalTokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusOK {
		t.Errorf("expected 200 (no auth header), got %d", w.Code)
	}
}

func TestOptionalTokenAuth_BasicAuth_ExtractsToken(t *testing.T) {
	// Basic auth is now supported: password portion is used as token.
	// With a valid token in DB, Basic auth should succeed.
	fixture := setupTestService(t, false)
	seedUserWithToken(t, fixture, "basic-user", "pass")
	handler := OptionalTokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "Basic dXNlcjpwYXNz") // user:pass
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusOK {
		t.Errorf("expected 200 for valid Basic auth token, got %d", w.Code)
	}
}

func TestOptionalTokenAuth_BasicAuth_InvalidToken(t *testing.T) {
	// Basic auth with an invalid token should be rejected.
	fixture := setupTestService(t, false)
	seedUserWithToken(t, fixture, "valid-user", "valid-token")
	handler := OptionalTokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "Basic dXNlcjpwYXNz") // user:pass (not valid-token)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected 401 for invalid Basic auth token, got %d", w.Code)
	}
}

func TestOptionalTokenAuth_UnrecognizedScheme(t *testing.T) {
	fixture := setupTestService(t, false)
	handler := OptionalTokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "Digest realm=test")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected 401 for unrecognized auth scheme, got %d", w.Code)
	}
}

func TestOptionalTokenAuth_NoTokensInDB_DefaultRejects(t *testing.T) {
	// Default (AllowAnyToken=false): optional auth with empty token table rejects
	fixture := setupTestService(t, false)
	handler := OptionalTokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "token anyvalue")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected 401 with default secure mode, got %d", w.Code)
	}
}

func TestOptionalTokenAuth_NoTokensInDB_AllowAnyToken(t *testing.T) {
	// AllowAnyToken=true: optional auth with empty token table accepts
	fixture := setupTestService(t, true)
	if err := fixture.db().Create(&db.User{Login: "admin", Name: "admin", Type: db.TypeUser, SiteAdmin: true}).Error; err != nil {
		t.Fatalf("create admin: %v", err)
	}
	handler := OptionalTokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "token anyvalue")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusOK {
		t.Errorf("expected 200 with AllowAnyToken, got %d", w.Code)
	}
}

func TestOptionalTokenAuth_InvalidToken_WithTokensInDB(t *testing.T) {
	fixture := setupTestService(t, false)
	seedUserWithToken(t, fixture, "valid-user", "valid-token")

	handler := OptionalTokenAuth(fixture.svc)(okHandler)

	r := fixture.request("GET", "/")
	r.Header.Set("Authorization", "token wrong-token")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected 401 for invalid token with tokens in DB, got %d", w.Code)
	}
	var body map[string]any
	if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
		t.Fatalf("failed to decode body: %v", err)
	}
	if body["message"] != "Bad credentials" {
		t.Errorf("unexpected message: %v", body["message"])
	}
}

func TestMaxBodySize(t *testing.T) {
	handler := MaxBodySize(10)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
			return
		}
		w.WriteHeader(http.StatusOK)
	}))

	// Small body — should pass
	r := httptest.NewRequest("POST", "/", strings.NewReader("short"))
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)
	if w.Code != http.StatusOK {
		t.Errorf("expected 200 for small body, got %d", w.Code)
	}

	// Large body — should fail when handler reads
	r = httptest.NewRequest("POST", "/", strings.NewReader("this body exceeds the ten byte limit"))
	w = httptest.NewRecorder()
	handler.ServeHTTP(w, r)
	if w.Code != http.StatusRequestEntityTooLarge {
		t.Errorf("expected 413 for large body, got %d", w.Code)
	}
}

func TestMaxBodySizeUnless(t *testing.T) {
	handler := MaxBodySizeUnless(10, func(r *http.Request) bool {
		return r.URL.Path == "/skip"
	})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if _, err := io.ReadAll(r.Body); err != nil {
			http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
			return
		}
		w.WriteHeader(http.StatusOK)
	}))

	r := httptest.NewRequest("POST", "/skip", strings.NewReader("this body exceeds the ten byte limit"))
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, r)
	if w.Code != http.StatusOK {
		t.Errorf("expected skipped route to bypass body limit, got %d", w.Code)
	}

	r = httptest.NewRequest("POST", "/enforce", strings.NewReader("this body exceeds the ten byte limit"))
	w = httptest.NewRecorder()
	handler.ServeHTTP(w, r)
	if w.Code != http.StatusRequestEntityTooLarge {
		t.Errorf("expected non-skipped route to enforce body limit, got %d", w.Code)
	}
}
