package api_test

import (
	"bytes"
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/jackc/pgx/v5"

	"archivmail/config"
	"archivmail/internal/api"
	"archivmail/internal/audit"
	"archivmail/internal/auth"
	"archivmail/internal/index"
	"archivmail/internal/storage"
	"archivmail/internal/userstore"
)

type testEnv struct {
	server *api.Server
	users  *userstore.Store
	store  *storage.Store
	idx    index.Indexer
}

func newTestEnv(t *testing.T) *testEnv {
	t.Helper()
	dir := t.TempDir()
	logger := slog.New(slog.NewTextHandler(os.Discard, nil))

	store, err := storage.New(filepath.Join(dir, "store"))
	if err != nil {
		t.Fatal(err)
	}

	idx, err := index.New(filepath.Join(dir, "index"), 100, "manticore")
	if err != nil {
		t.Skip("manticore not available:", err)
	}

	dsn := os.Getenv("TEST_DATABASE_URL")
	if dsn == "" {
		t.Skip("TEST_DATABASE_URL not set — skipping (needs PostgreSQL)")
	}

	// Create isolated schemas for this test
	schemaUsers := "apitest_users_" + strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_"))
	schemaAudit := "apitest_audit_" + strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_"))
	if len(schemaUsers) > 63 {
		schemaUsers = schemaUsers[:63]
	}
	if len(schemaAudit) > 63 {
		schemaAudit = schemaAudit[:63]
	}

	ctx := context.Background()
	conn, err := pgx.Connect(ctx, dsn)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	conn.Exec(ctx, "CREATE SCHEMA IF NOT EXISTS "+schemaUsers)
	conn.Exec(ctx, "CREATE SCHEMA IF NOT EXISTS "+schemaAudit)
	conn.Close(ctx)

	sep := "?"
	if strings.Contains(dsn, "?") {
		sep = "&"
	}
	usersDSN := dsn + sep + "search_path=" + schemaUsers
	auditDSN := dsn + sep + "search_path=" + schemaAudit

	users, err := userstore.New(usersDSN)
	if err != nil {
		t.Fatalf("userstore.New: %v", err)
	}

	audlog, err := audit.New(auditDSN, dir, logger)
	if err != nil {
		t.Fatalf("audit.New: %v", err)
	}

	// Seed users
	users.Create(userstore.CreateUserRequest{Username: "admin", Email: "admin@x.com", Password: "adminpass", Role: userstore.RoleAdmin})
	users.Create(userstore.CreateUserRequest{Username: "auditor", Email: "auditor@x.com", Password: "auditorpass", Role: userstore.RoleAuditor})
	users.Create(userstore.CreateUserRequest{Username: "user1", Email: "user1@x.com", Password: "userpass", Role: userstore.RoleUser})

	authMgr := auth.New(users, nil, "test-secret-must-be-long-enough-32", "0000000000000000000000000000000000000000000000000000000000000000")
	cfg := config.APIConfig{Bind: ":18080", Secret: "test-secret-must-be-long-enough-32"}
	srv := api.New(cfg, store, idx, authMgr, users, audlog, logger)

	t.Cleanup(func() {
		idx.Close()
		users.Close()
		audlog.Close()
		conn2, _ := pgx.Connect(context.Background(), dsn)
		if conn2 != nil {
			conn2.Exec(context.Background(), "DROP SCHEMA "+schemaUsers+" CASCADE")
			conn2.Exec(context.Background(), "DROP SCHEMA "+schemaAudit+" CASCADE")
			conn2.Close(context.Background())
		}
	})

	return &testEnv{server: srv, users: users, store: store, idx: idx}
}

func (e *testEnv) do(t *testing.T, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		json.NewEncoder(&buf).Encode(body)
	}
	req := httptest.NewRequest(method, path, &buf)
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	w := httptest.NewRecorder()
	e.server.ServeHTTP(w, req)
	return w
}

func (e *testEnv) login(t *testing.T, username, password string) string {
	t.Helper()
	w := e.do(t, "POST", "/api/auth/login",
		map[string]string{"username": username, "password": password}, "")
	if w.Code != 200 {
		t.Fatalf("login %s: status %d, body: %s", username, w.Code, w.Body.String())
	}
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	return resp["token"].(string)
}

// ---- Tests ----

func TestHealth(t *testing.T) {
	env := newTestEnv(t)
	w := env.do(t, "GET", "/api/health", nil, "")
	if w.Code != 200 {
		t.Errorf("health: status %d", w.Code)
	}
}

func TestLoginAndMe(t *testing.T) {
	env := newTestEnv(t)
	token := env.login(t, "admin", "adminpass")

	w := env.do(t, "GET", "/api/auth/me", nil, token)
	if w.Code != 200 {
		t.Fatalf("me: status %d", w.Code)
	}
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["username"] != "admin" {
		t.Errorf("me username = %q", resp["username"])
	}
	if resp["role"] != "admin" {
		t.Errorf("me role = %q", resp["role"])
	}
}

func TestLoginWrongCredentials(t *testing.T) {
	env := newTestEnv(t)
	w := env.do(t, "POST", "/api/auth/login",
		map[string]string{"username": "admin", "password": "wrong"}, "")
	if w.Code != 401 {
		t.Errorf("expected 401, got %d", w.Code)
	}
}

func TestUnauthenticatedSearchBlocked(t *testing.T) {
	env := newTestEnv(t)
	w := env.do(t, "GET", "/api/search?q=test", nil, "")
	if w.Code != 401 {
		t.Errorf("expected 401, got %d", w.Code)
	}
}

func TestLogout(t *testing.T) {
	env := newTestEnv(t)
	token := env.login(t, "admin", "adminpass")

	w := env.do(t, "POST", "/api/auth/logout", nil, token)
	if w.Code != 200 {
		t.Fatalf("logout: status %d", w.Code)
	}

	// Token should now be invalid
	w2 := env.do(t, "GET", "/api/auth/me", nil, token)
	if w2.Code != 401 {
		t.Errorf("after logout, me should return 401, got %d", w2.Code)
	}
}

func TestAdminUserCRUD(t *testing.T) {
	env := newTestEnv(t)
	token := env.login(t, "admin", "adminpass")

	// List users
	w := env.do(t, "GET", "/api/users", nil, token)
	if w.Code != 200 {
		t.Fatalf("list users: status %d", w.Code)
	}
	var users []map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &users)
	if len(users) != 3 { // admin + auditor + user1
		t.Errorf("expected 3 users, got %d", len(users))
	}

	// Create user
	w = env.do(t, "POST", "/api/users",
		map[string]string{"username": "newuser", "email": "new@x.com", "password": "pw123", "role": "user"},
		token)
	if w.Code != 201 {
		t.Fatalf("create user: status %d, body: %s", w.Code, w.Body.String())
	}
}

func TestNonAdminCannotManageUsers(t *testing.T) {
	env := newTestEnv(t)
	token := env.login(t, "user1", "userpass")

	w := env.do(t, "GET", "/api/users", nil, token)
	if w.Code != 403 {
		t.Errorf("user role should not list users, got %d", w.Code)
	}
}

func TestAuditorCanAccessAuditLog(t *testing.T) {
	env := newTestEnv(t)
	token := env.login(t, "auditor", "auditorpass")

	w := env.do(t, "GET", "/api/audit", nil, token)
	if w.Code != 200 {
		t.Errorf("auditor should access audit log, got %d", w.Code)
	}
}

func TestUserCannotAccessAuditLog(t *testing.T) {
	env := newTestEnv(t)
	token := env.login(t, "user1", "userpass")

	w := env.do(t, "GET", "/api/audit", nil, token)
	if w.Code != 403 {
		t.Errorf("user role should not access audit log, got %d", w.Code)
	}
}

func TestSearchReturnsResults(t *testing.T) {
	env := newTestEnv(t)
	token := env.login(t, "admin", "adminpass")

	w := env.do(t, "GET", "/api/search?q=test", nil, token)
	if w.Code != 200 {
		t.Fatalf("search: status %d, body: %s", w.Code, w.Body.String())
	}
	var result map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &result)
	if _, ok := result["total"]; !ok {
		t.Error("search response missing 'total' field")
	}
}
