package handlers

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"golang.org/x/crypto/bcrypt"

	"switchboard-core/auth"
	"switchboard-core/config"
	"switchboard-core/store"
)

func testConfig() *config.Config {
	return &config.Config{
		JWTSecret: "test-secret-key-for-unit-tests",
		AuthMode:  "builtin",
	}
}

// testAuthHandler creates an AuthHandler with nil stores (safe for non-DB tests).
func testAuthHandler() *AuthHandler {
	return NewAuthHandler(testConfig(), store.Stores{}, nil, auth.NewBuiltinProvider())
}

// ── JWT Token Tests ─────────────────────────

func TestJWTGeneration(t *testing.T) {
	cfg := testConfig()

	now := time.Now()
	claims := Claims{
		UserID: "550e8400-e29b-41d4-a716-446655440000",
		Email:  "test@example.com",
		RegisteredClaims: jwt.RegisteredClaims{
			IssuedAt:  jwt.NewNumericDate(now),
			ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
			Issuer:    "switchboard-core",
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(cfg.JWTSecret))
	if err != nil {
		t.Fatalf("Failed to sign token: %v", err)
	}

	if tokenString == "" {
		t.Fatal("Token string is empty")
	}

	// Parse it back
	parsed := &Claims{}
	parsedToken, err := jwt.ParseWithClaims(tokenString, parsed, func(t *jwt.Token) (interface{}, error) {
		return []byte(cfg.JWTSecret), nil
	})
	if err != nil {
		t.Fatalf("Failed to parse token: %v", err)
	}
	if !parsedToken.Valid {
		t.Fatal("Parsed token is not valid")
	}
	if parsed.UserID != claims.UserID {
		t.Errorf("UserID mismatch: got %s, want %s", parsed.UserID, claims.UserID)
	}
	if parsed.Email != claims.Email {
		t.Errorf("Email mismatch: got %s, want %s", parsed.Email, claims.Email)
	}
}

func TestJWTExpired(t *testing.T) {
	cfg := testConfig()

	claims := Claims{
		UserID: "test-user",
		Email:  "test@example.com",
		RegisteredClaims: jwt.RegisteredClaims{
			IssuedAt:  jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(-30 * time.Minute)),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, _ := token.SignedString([]byte(cfg.JWTSecret))

	parsed := &Claims{}
	_, err := jwt.ParseWithClaims(tokenString, parsed, func(t *jwt.Token) (interface{}, error) {
		return []byte(cfg.JWTSecret), nil
	})
	if err == nil {
		t.Fatal("Expected error for expired token, got nil")
	}
}

func TestJWTWrongSecret(t *testing.T) {
	claims := Claims{
		UserID: "test-user",
		Email:  "test@example.com",
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, _ := token.SignedString([]byte("correct-secret"))

	parsed := &Claims{}
	_, err := jwt.ParseWithClaims(tokenString, parsed, func(t *jwt.Token) (interface{}, error) {
		return []byte("wrong-secret"), nil
	})
	if err == nil {
		t.Fatal("Expected error for wrong secret, got nil")
	}
}

// ── Password Hashing Tests ─────────────────

func TestBcryptHash(t *testing.T) {
	password := "mySecurePassword123!"

	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
	if err != nil {
		t.Fatalf("Failed to hash: %v", err)
	}

	err = bcrypt.CompareHashAndPassword(hash, []byte(password))
	if err != nil {
		t.Error("Correct password should match hash")
	}

	err = bcrypt.CompareHashAndPassword(hash, []byte("wrongPassword"))
	if err == nil {
		t.Error("Wrong password should not match hash")
	}
}

func TestBcryptDifferentHashesForSamePassword(t *testing.T) {
	password := "samePassword"

	hash1, _ := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
	hash2, _ := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)

	if string(hash1) == string(hash2) {
		t.Error("Same password should produce different hashes (salt)")
	}

	if bcrypt.CompareHashAndPassword(hash1, []byte(password)) != nil {
		t.Error("hash1 should verify")
	}
	if bcrypt.CompareHashAndPassword(hash2, []byte(password)) != nil {
		t.Error("hash2 should verify")
	}
}

// ── Token Hashing Tests ────────────────────

func TestTokenHash(t *testing.T) {
	token := "abc123refreshtoken"
	hash := hashToken(token)

	if hashToken(token) != hash {
		t.Error("hashToken should be deterministic")
	}

	if hashToken("different") == hash {
		t.Error("Different tokens should produce different hashes")
	}

	if len(hash) != 64 {
		t.Errorf("Expected 64 char hex hash, got %d chars", len(hash))
	}

	expected := sha256.Sum256([]byte(token))
	expectedHex := hex.EncodeToString(expected[:])
	if hash != expectedHex {
		t.Errorf("Hash mismatch: got %s, want %s", hash, expectedHex)
	}
}

// ── Request Validation Tests ────────────────

func TestRegisterValidation(t *testing.T) {
	h := testAuthHandler()

	tests := []struct {
		name     string
		body     string
		wantCode int
	}{
		{
			name:     "missing fields",
			body:     `{}`,
			wantCode: http.StatusBadRequest,
		},
		{
			name:     "missing password",
			body:     `{"username":"test","email":"test@example.com"}`,
			wantCode: http.StatusBadRequest,
		},
		{
			name:     "password too short",
			body:     `{"username":"test","email":"test@example.com","password":"short"}`,
			wantCode: http.StatusBadRequest,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := httptest.NewRecorder()
			c, _ := gin.CreateTestContext(w)
			c.Request = httptest.NewRequest("POST", "/api/v1/auth/register",
				strings.NewReader(tt.body))
			c.Request.Header.Set("Content-Type", "application/json")

			h.Register(c)

			if w.Code != tt.wantCode {
				t.Errorf("got status %d, want %d. Body: %s",
					w.Code, tt.wantCode, w.Body.String())
			}
		})
	}
}

func TestLoginValidation(t *testing.T) {
	h := testAuthHandler()

	tests := []struct {
		name     string
		body     string
		wantCode int
	}{
		{
			name:     "missing fields",
			body:     `{}`,
			wantCode: http.StatusBadRequest,
		},
		{
			name:     "missing password",
			body:     `{"login":"test@example.com"}`,
			wantCode: http.StatusBadRequest,
		},
		{
			name:     "missing login",
			body:     `{"password":"12345678"}`,
			wantCode: http.StatusBadRequest,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := httptest.NewRecorder()
			c, _ := gin.CreateTestContext(w)
			c.Request = httptest.NewRequest("POST", "/api/v1/auth/login",
				strings.NewReader(tt.body))
			c.Request.Header.Set("Content-Type", "application/json")

			h.Login(c)

			if w.Code != tt.wantCode {
				t.Errorf("got status %d, want %d", w.Code, tt.wantCode)
			}
		})
	}
}

func TestRefreshValidation(t *testing.T) {
	h := testAuthHandler()

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("POST", "/api/v1/auth/refresh",
		strings.NewReader(`{}`))
	c.Request.Header.Set("Content-Type", "application/json")

	h.Refresh(c)

	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected 400 for missing refresh_token, got %d", w.Code)
	}
}

func TestLogoutWithoutToken(t *testing.T) {
	h := testAuthHandler()

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("POST", "/api/v1/auth/logout",
		strings.NewReader(`{}`))
	c.Request.Header.Set("Content-Type", "application/json")

	h.Logout(c)

	if w.Code != http.StatusOK {
		t.Errorf("Logout without token should succeed, got %d", w.Code)
	}

	var resp map[string]string
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["message"] != "logged out" {
		t.Errorf("Expected 'logged out' message, got %s", resp["message"])
	}
}
