package api

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"crypto-miner-server/internal/builder"
	"crypto-miner-server/internal/db"
	"crypto-miner-server/internal/models"
	"crypto-miner-server/internal/pool"
)

const routerAuthCacheTTL = 5 * time.Minute

func resetAuthState(t *testing.T) {
	t.Helper()
	authSessionCacheMu.Lock()
	authSessionCache = map[string]time.Time{}
	authSessionCacheMu.Unlock()
	usersMu.Lock()
	authUsers = map[string]string{}
	usersFilePath = ""
	usersMu.Unlock()
	authLoadMu.Lock()
	authLoadedDataDir = ""
	authLoadMu.Unlock()
	SetAgentPathSecret("")
	SetRotateSecretFn(nil)
	t.Cleanup(resetAuthGlobals)
}

func resetAuthGlobals() {
	authSessionCacheMu.Lock()
	authSessionCache = map[string]time.Time{}
	authSessionCacheMu.Unlock()
	SetAgentPathSecret("")
	SetRotateSecretFn(nil)
}

func TestRouterConstants(t *testing.T) {
	if authCacheTTL != routerAuthCacheTTL {
		t.Fatalf("authCacheTTL = %v, want %v", authCacheTTL, routerAuthCacheTTL)
	}
}

func TestAuthCacheKeyDeterministic(t *testing.T) {
	k1 := authCacheKey("user", "pass")
	k2 := authCacheKey("user", "pass")
	if k1 != k2 || k1 == "" {
		t.Fatalf("cache key not stable: %q %q", k1, k2)
	}
	if authCacheKey("user", "other") == k1 {
		t.Fatal("different passwords should produce different cache keys")
	}
}

func TestBasicAuthMiddlewareOptionsPassthrough(t *testing.T) {
	resetAuthState(t)
	called := false
	h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		w.WriteHeader(http.StatusNoContent)
	}))
	req := httptest.NewRequest(http.MethodOptions, "/api/v1/config", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if !called || rec.Code != http.StatusNoContent {
		t.Fatalf("OPTIONS should bypass auth: called=%v status=%d", called, rec.Code)
	}
}

func TestBasicAuthMiddlewareHealthPublic(t *testing.T) {
	resetAuthState(t)
	h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("health should be public, got %d", rec.Code)
	}
}

// TestBasicAuthMiddlewareBuildDownloadRequiresAuth verifies that build download
// routes are no longer publicly accessible — they require fleet secret or Basic Auth.
func TestBasicAuthMiddlewareBuildDownloadRequiresAuth(t *testing.T) {
	resetAuthState(t)
	h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	paths := []string{
		"/api/v1/builds/abc/download",
		"/api/v1/builds/abc/artifact/worker.exe",
	}
	for _, p := range paths {
		req := httptest.NewRequest(http.MethodGet, p, nil)
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, req)
		if rec.Code != http.StatusUnauthorized {
			t.Fatalf("%s without auth should be 401, got %d", p, rec.Code)
		}
	}

	// Fleet secret should grant access.
	SetAgentPathSecret("test-secret-abc")
	t.Cleanup(func() { SetAgentPathSecret("") })
	for _, p := range paths {
		req := httptest.NewRequest(http.MethodGet, p, nil)
		req.Header.Set("X-Fleet-Secret", "test-secret-abc")
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, req)
		if rec.Code != http.StatusOK {
			t.Fatalf("%s with fleet secret should be 200, got %d", p, rec.Code)
		}
	}
}

func TestBasicAuthMiddlewareDropperPublic(t *testing.T) {
	resetAuthState(t)
	h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	for _, path := range []string{"/get", "/install.sh", "/install.ps1"} {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, req)
		if rec.Code != http.StatusOK {
			t.Fatalf("%s should be public, got %d", path, rec.Code)
		}
	}
}

func TestBasicAuthMiddlewareMissingCredentials(t *testing.T) {
	resetAuthState(t)
	usersMu.Lock()
	authUsers["admin"] = "secret"
	usersMu.Unlock()

	h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("handler should not run without auth")
	}))
	req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("expected 401, got %d", rec.Code)
	}
	if !strings.Contains(rec.Header().Get("WWW-Authenticate"), "Basic") {
		t.Fatal("expected WWW-Authenticate header")
	}
}

func TestBasicAuthMiddlewareWrongPassword(t *testing.T) {
	resetAuthState(t)
	hashed, err := hashPassword("correct")
	if err != nil {
		t.Fatal(err)
	}
	usersMu.Lock()
	authUsers["admin"] = hashed
	usersMu.Unlock()

	h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("handler should not run with bad password")
	}))
	req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
	req.SetBasicAuth("admin", "wrong")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("expected 401, got %d", rec.Code)
	}
}

func TestBasicAuthMiddlewareValidCredentials(t *testing.T) {
	resetAuthState(t)
	hashed, err := hashPassword("correct")
	if err != nil {
		t.Fatal(err)
	}
	usersMu.Lock()
	authUsers["admin"] = hashed
	usersMu.Unlock()

	h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
	req.SetBasicAuth("admin", "correct")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}
	if !authCacheHit("admin", "correct") {
		t.Fatal("successful auth should populate cache")
	}
}

func TestBasicAuthMiddlewareAgentPathFleetSecret(t *testing.T) {
	resetAuthState(t)
	SetAgentPathSecret("fleet-secret-123")

	h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))

	req := httptest.NewRequest(http.MethodPost, "/api/v1/agent/decide", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusForbidden {
		t.Fatalf("missing fleet secret should be 403, got %d", rec.Code)
	}

	req = httptest.NewRequest(http.MethodPost, "/api/v1/agent/decide", nil)
	req.Header.Set("X-Fleet-Secret", "fleet-secret-123")
	rec = httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("valid fleet secret should pass, got %d", rec.Code)
	}
}

func TestRouterPostUsersValidation(t *testing.T) {
	router, _, _, _ := newTestRouter(t)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewReader([]byte(`{}`)))
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("empty payload should be 400, got %d body=%s", rec.Code, rec.Body.String())
	}
}

func TestRouterPostUsersSuccess(t *testing.T) {
	router, _, _, dataDir := newTestRouter(t)

	body, _ := json.Marshal(map[string]string{"username": "newop", "password": "newpass"})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewReader(body))
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
	}

	usersPath := filepath.Join(dataDir, "users.json")
	data, err := os.ReadFile(usersPath)
	if err != nil {
		t.Fatal(err)
	}
	var users map[string]string
	if err := json.Unmarshal(data, &users); err != nil {
		t.Fatal(err)
	}
	if !checkPassword(users["newop"], "newpass") {
		t.Fatal("new user password should be bcrypt stored and verifiable")
	}
}

func TestRouterRotateSecretNotConfigured(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/server/rotate-secret", nil)
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusServiceUnavailable {
		t.Fatalf("expected 503, got %d body=%s", rec.Code, rec.Body.String())
	}
}

func TestRouterRotateSecretSuccess(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	SetRotateSecretFn(func() (string, error) {
		return "new-secret-token-xyz", nil
	})
	t.Cleanup(func() { SetRotateSecretFn(nil) })

	req := httptest.NewRequest(http.MethodPost, "/api/v1/server/rotate-secret", nil)
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if body["ok"] != true {
		t.Fatalf("unexpected body: %v", body)
	}
}

func TestRouterBuilderCancelNotFound(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodDelete, "/api/v1/builder/cancel/missing-token", nil)
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("expected 404, got %d body=%s", rec.Code, rec.Body.String())
	}
}

// TestRouterBuildDownloadAuth verifies that build download routes require either
// the fleet secret (X-Fleet-Secret header) or Basic Auth — they are no longer
// publicly accessible without credentials.
func TestRouterBuildDownloadAuth(t *testing.T) {
	dataDir := t.TempDir()
	seedTestUsers(t, dataDir)
	database, err := db.New(dataDir)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { database.Close() })

	buildID := "dl-build"
	buildDir := filepath.Join(dataDir, "builds", buildID)
	if err := os.MkdirAll(buildDir, 0755); err != nil {
		t.Fatal(err)
	}
	binPath := filepath.Join(buildDir, "agent.exe")
	if err := os.WriteFile(binPath, []byte("fake-binary"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := database.InsertBuild(&models.BuildRecord{
		ID: buildID, WorkerName: "w", ServerURL: "http://x", Wallet: "48x",
		FilePath: binPath, FileName: "agent.exe", Platform: "windows",
	}); err != nil {
		t.Fatal(err)
	}

	const testSecret = "test-fleet-secret-12345"
	SetAgentPathSecret(testSecret)
	t.Cleanup(func() { SetAgentPathSecret("") })

	wsHub := NewWSHub(database)
	cfg := &mockConfigProvider{}
	configHandler := NewConfigHandler(cfg)
	aiHandler := NewAIHandler(database)
	fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{})
	builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
	blueprintHandler := NewBlueprintHandler(dataDir)
	router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, NewDropperHandler(database, nil), nil, "", dataDir, nil)

	dlURL := "/api/v1/builds/" + buildID + "/download"

	// 1. Unauthenticated → 401.
	req := httptest.NewRequest(http.MethodGet, dlURL, nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("unauthenticated download should be 401, got %d", rec.Code)
	}

	// 2. Valid fleet secret → 200.
	req = httptest.NewRequest(http.MethodGet, dlURL, nil)
	req.Header.Set("X-Fleet-Secret", testSecret)
	rec = httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("fleet-secret download should be 200, got %d body=%s", rec.Code, rec.Body.String())
	}

	// 3. Basic Auth → 200.
	req = httptest.NewRequest(http.MethodGet, dlURL, nil)
	req.SetBasicAuth("testuser", "testpass")
	rec = httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("basic-auth download should be 200, got %d body=%s", rec.Code, rec.Body.String())
	}
}

func TestRouterDropperInstallScriptsPublic(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	for _, path := range []string{"/install.sh", "/install.ps1"} {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		req.Host = "forge.local:8989"
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)
		if rec.Code != http.StatusOK {
			t.Fatalf("%s status=%d", path, rec.Code)
		}
		if !strings.Contains(rec.Body.String(), "AetherForge") {
			t.Fatalf("%s missing branding", path)
		}
	}
}

func TestRouterSPAFallbackUnknownRoute(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/unknown-dashboard-route", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("expected SPA fallback 200, got %d", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "AetherForge") {
		t.Fatal("expected index.html fallback")
	}
}

func TestRouterNoWebRootFallback(t *testing.T) {
	dataDir := t.TempDir()
	seedTestUsers(t, dataDir)
	database, err := db.New(dataDir)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { database.Close() })

	wsHub := NewWSHub(database)
	cfg := &mockConfigProvider{}
	configHandler := NewConfigHandler(cfg)
	aiHandler := NewAIHandler(database)
	fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{})
	builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
	blueprintHandler := NewBlueprintHandler(dataDir)

	router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, nil, "", dataDir, nil)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "No frontend configured") {
		t.Fatalf("unexpected body: %s", rec.Body.String())
	}
}

func TestLoadUsersCreatesAndReloadsLoginSidecar(t *testing.T) {
	resetAuthState(t)
	dataDir := t.TempDir()

	LoadUsers(dataDir)

	sidecarPath := filepath.Join(dataDir, "login-credentials.json")
	data, err := os.ReadFile(sidecarPath)
	if err != nil {
		t.Fatalf("login sidecar: %v", err)
	}
	var creds map[string]string
	if err := json.Unmarshal(data, &creds); err != nil {
		t.Fatal(err)
	}
	pw, ok := creds["admin"]
	if !ok || pw == "" {
		t.Fatalf("expected admin password in sidecar: %v", creds)
	}
	if !checkPassword(authUsers["admin"], pw) {
		t.Fatal("sidecar password should match users.json hash")
	}

	if err := saveUser("admin", "new-secret-pass"); err != nil {
		t.Fatal(err)
	}
	reloaded, err := readLoginSidecar(sidecarPath)
	if err != nil || reloaded["admin"] != "new-secret-pass" {
		t.Fatalf("sidecar not updated after saveUser: %v err=%v", reloaded, err)
	}
}
