package server

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"log/slog"

	"github.com/go-chi/chi/v5"

	"git.wntrmute.dev/mc/metacrypt/internal/barrier"
	"git.wntrmute.dev/mc/metacrypt/internal/config"
	"git.wntrmute.dev/mc/metacrypt/internal/crypto"

	mcdslauth "git.wntrmute.dev/mc/mcdsl/auth"
	mcdslconfig "git.wntrmute.dev/mc/mcdsl/config"
	"git.wntrmute.dev/mc/metacrypt/internal/db"
	"git.wntrmute.dev/mc/metacrypt/internal/engine"
	"git.wntrmute.dev/mc/metacrypt/internal/policy"
	"git.wntrmute.dev/mc/metacrypt/internal/seal"

	// auth is used indirectly via the server
	"git.wntrmute.dev/mc/metacrypt/internal/auth"
)

func setupTestServer(t *testing.T) (*Server, *seal.Manager, chi.Router) {
	t.Helper()
	dir := t.TempDir()
	database, err := db.Open(filepath.Join(dir, "test.db"))
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	t.Cleanup(func() { _ = database.Close() })
	_ = db.Migrate(database)

	b := barrier.NewAESGCMBarrier(database)
	sealMgr := seal.NewManager(database, b, nil, slog.Default())
	_ = sealMgr.CheckInitialized()

	// Auth not exercised in these tests — token info is injected directly.
	authenticator, _ := auth.NewAuthenticator(auth.Config{ServerURL: "http://localhost:0"}, slog.Default())
	policyEngine := policy.NewEngine(b)
	engineRegistry := engine.NewRegistry(b, slog.Default())

	cfg := &config.Config{
		Server: config.ServerConfig{
			ServerConfig: mcdslconfig.ServerConfig{
				ListenAddr: ":0",
				TLSCert:    "cert.pem",
				TLSKey:     "key.pem",
			},
		},
		Database: mcdslconfig.DatabaseConfig{Path: filepath.Join(dir, "test.db")},
		MCIAS:    config.MCIASConfig{Config: mcdslauth.Config{ServerURL: "https://mcias.test"}},
		Seal: config.SealConfig{
			Argon2Time:    1,
			Argon2Memory:  64 * 1024,
			Argon2Threads: 1,
		},
	}

	logger := slog.Default()
	srv := New(cfg, database, sealMgr, authenticator, policyEngine, engineRegistry, nil, logger, "test")

	r := chi.NewRouter()
	srv.registerRoutes(r)
	return srv, sealMgr, r
}

func TestStatusEndpoint(t *testing.T) {
	_, _, mux := setupTestServer(t)

	req := httptest.NewRequest(http.MethodGet, "/v1/status", nil)
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status code: got %d, want %d", w.Code, http.StatusOK)
	}

	var resp map[string]interface{}
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["state"] != "uninitialized" {
		t.Errorf("state: got %q, want %q", resp["state"], "uninitialized")
	}
}

func TestInitEndpoint(t *testing.T) {
	_, _, mux := setupTestServer(t)

	body := `{"password":"test-password"}`
	req := httptest.NewRequest(http.MethodPost, "/v1/init", strings.NewReader(body))
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status code: got %d, want %d. Body: %s", w.Code, http.StatusOK, w.Body.String())
	}

	var resp map[string]interface{}
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["state"] != "unsealed" {
		t.Errorf("state: got %q, want %q", resp["state"], "unsealed")
	}

	// Second init should fail.
	req2 := httptest.NewRequest(http.MethodPost, "/v1/init", strings.NewReader(body))
	w2 := httptest.NewRecorder()
	mux.ServeHTTP(w2, req2)
	if w2.Code != http.StatusConflict {
		t.Errorf("double init: got %d, want %d", w2.Code, http.StatusConflict)
	}
}

func TestUnsealEndpoint(t *testing.T) {
	_, sealMgr, mux := setupTestServer(t)

	// Initialize first.
	params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
	_ = sealMgr.Initialize(context.Background(), []byte("password"), params)
	_ = sealMgr.Seal()

	// Unseal with wrong password.
	body := `{"password":"wrong"}`
	req := httptest.NewRequest(http.MethodPost, "/v1/unseal", strings.NewReader(body))
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)
	if w.Code != http.StatusUnauthorized {
		t.Errorf("wrong password: got %d, want %d", w.Code, http.StatusUnauthorized)
	}

	// Unseal with correct password.
	body = `{"password":"password"}`
	req = httptest.NewRequest(http.MethodPost, "/v1/unseal", strings.NewReader(body))
	w = httptest.NewRecorder()
	mux.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("correct password: got %d, want %d. Body: %s", w.Code, http.StatusOK, w.Body.String())
	}
}

func TestStatusMethodNotAllowed(t *testing.T) {
	_, _, mux := setupTestServer(t)

	req := httptest.NewRequest(http.MethodPost, "/v1/status", nil)
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)
	if w.Code != http.StatusMethodNotAllowed {
		t.Errorf("POST /v1/status: got %d, want %d", w.Code, http.StatusMethodNotAllowed)
	}
}

func TestRootNotFound(t *testing.T) {
	_, _, mux := setupTestServer(t)

	// The vault server no longer serves a web UI at /; that is handled by metacrypt-web.
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)
	if w.Code != http.StatusNotFound {
		t.Errorf("root: got %d, want %d", w.Code, http.StatusNotFound)
	}
}

func unsealServer(t *testing.T, sealMgr *seal.Manager, _ interface{}) {
	t.Helper()
	params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
	if err := sealMgr.Initialize(context.Background(), []byte("password"), params); err != nil {
		t.Fatalf("initialize: %v", err)
	}
}

func makeEngineRequest(mount, operation string) string {
	return `{"mount":"` + mount + `","operation":"` + operation + `","data":{}}`
}

// stubEngine is a minimal engine implementation for testing the generic endpoint.
type stubEngine struct {
	engineType engine.EngineType
}

func (e *stubEngine) Type() engine.EngineType { return e.engineType }
func (e *stubEngine) Initialize(_ context.Context, _ barrier.Barrier, _ string, _ map[string]interface{}) error {
	return nil
}
func (e *stubEngine) Unseal(_ context.Context, _ barrier.Barrier, _ string) error { return nil }
func (e *stubEngine) Seal() error                                                 { return nil }
func (e *stubEngine) HandleRequest(_ context.Context, req *engine.Request) (*engine.Response, error) {
	return &engine.Response{Data: map[string]interface{}{"ok": true}}, nil
}

// mountStubEngine registers a factory and mounts a stub engine of the given type.
func mountStubEngine(t *testing.T, srv *Server, name string, engineType engine.EngineType) {
	t.Helper()
	srv.engines.RegisterFactory(engineType, func() engine.Engine {
		return &stubEngine{engineType: engineType}
	})
	if err := srv.engines.Mount(context.Background(), name, engineType, nil); err != nil {
		// Ignore "already exists" from re-mounting the same name.
		if !strings.Contains(err.Error(), "already exists") {
			t.Fatalf("mount stub %q as %s: %v", name, engineType, err)
		}
	}
}

func withTokenInfo(r *http.Request, info *auth.TokenInfo) *http.Request {
	return r.WithContext(context.WithValue(r.Context(), tokenInfoKey, info))
}

// TestEngineRequestPolicyDeniesNonAdmin verifies that a non-admin user without
// an explicit allow rule is denied by the policy engine.
func TestEngineRequestPolicyDeniesNonAdmin(t *testing.T) {
	srv, sealMgr, _ := setupTestServer(t)
	unsealServer(t, sealMgr, nil)
	mountStubEngine(t, srv, "pki", engine.EngineTypeCA)

	body := makeEngineRequest("pki", "list-issuers")
	req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
	req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
	w := httptest.NewRecorder()
	srv.handleEngineRequest(w, req)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 Forbidden for non-admin without policy rule, got %d: %s", w.Code, w.Body.String())
	}
}

// TestEngineRequestPolicyAllowsAdmin verifies that admin users bypass policy.
func TestEngineRequestPolicyAllowsAdmin(t *testing.T) {
	srv, sealMgr, _ := setupTestServer(t)
	unsealServer(t, sealMgr, nil)
	mountStubEngine(t, srv, "pki", engine.EngineTypeCA)

	body := makeEngineRequest("pki", "list-issuers")
	req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
	req = withTokenInfo(req, &auth.TokenInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true})
	w := httptest.NewRecorder()
	srv.handleEngineRequest(w, req)

	// Admin bypasses policy; stub engine returns 200.
	if w.Code == http.StatusForbidden {
		t.Errorf("admin should not be forbidden by policy, got 403: %s", w.Body.String())
	}
}

// TestEngineRequestPolicyAllowsWithRule verifies that a non-admin user with an
// explicit allow rule is permitted to proceed.
func TestEngineRequestPolicyAllowsWithRule(t *testing.T) {
	srv, sealMgr, _ := setupTestServer(t)
	unsealServer(t, sealMgr, nil)
	mountStubEngine(t, srv, "pki", engine.EngineTypeCA)

	ctx := context.Background()
	_ = srv.policy.CreateRule(ctx, &policy.Rule{
		ID:        "allow-user-read",
		Priority:  100,
		Effect:    policy.EffectAllow,
		Roles:     []string{"user"},
		Resources: []string{"engine/*/*"},
		Actions:   []string{"read"},
	})

	body := makeEngineRequest("pki", "list-issuers")
	req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
	req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
	w := httptest.NewRecorder()
	srv.handleEngineRequest(w, req)

	// Policy allows; stub engine returns 200.
	if w.Code == http.StatusForbidden {
		t.Errorf("user with allow rule should not be forbidden, got 403: %s", w.Body.String())
	}
}

// TestEngineRequestAdminOnlyBlocksNonAdmin verifies that admin-only operations
// via the generic endpoint are rejected for non-admin users.
func TestEngineRequestAdminOnlyBlocksNonAdmin(t *testing.T) {
	srv, sealMgr, _ := setupTestServer(t)
	unsealServer(t, sealMgr, nil)

	// Mount stub engines so the admin-only lookup can resolve engine types.
	mountStubEngine(t, srv, "ca-mount", engine.EngineTypeCA)
	mountStubEngine(t, srv, "transit-mount", engine.EngineTypeTransit)
	mountStubEngine(t, srv, "sshca-mount", engine.EngineTypeSSHCA)
	mountStubEngine(t, srv, "user-mount", engine.EngineTypeUser)

	cases := []struct {
		mount string
		op    string
	}{
		{"ca-mount", "create-issuer"},
		{"ca-mount", "delete-cert"},
		{"transit-mount", "create-key"},
		{"transit-mount", "rotate-key"},
		{"sshca-mount", "create-profile"},
		{"user-mount", "provision"},
	}
	for _, tc := range cases {
		body := makeEngineRequest(tc.mount, tc.op)
		req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
		req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
		w := httptest.NewRecorder()
		srv.handleEngineRequest(w, req)

		if w.Code != http.StatusForbidden {
			t.Errorf("%s/%s: expected 403 for non-admin, got %d", tc.mount, tc.op, w.Code)
		}
	}
}

// TestEngineRequestAdminOnlyAllowsAdmin verifies that admin-only operations
// via the generic endpoint are allowed for admin users.
func TestEngineRequestAdminOnlyAllowsAdmin(t *testing.T) {
	srv, sealMgr, _ := setupTestServer(t)
	unsealServer(t, sealMgr, nil)

	mountStubEngine(t, srv, "ca-mount", engine.EngineTypeCA)
	mountStubEngine(t, srv, "transit-mount", engine.EngineTypeTransit)
	mountStubEngine(t, srv, "sshca-mount", engine.EngineTypeSSHCA)
	mountStubEngine(t, srv, "user-mount", engine.EngineTypeUser)

	cases := []struct {
		mount string
		op    string
	}{
		{"ca-mount", "create-issuer"},
		{"ca-mount", "delete-cert"},
		{"transit-mount", "create-key"},
		{"transit-mount", "rotate-key"},
		{"sshca-mount", "create-profile"},
		{"user-mount", "provision"},
	}
	for _, tc := range cases {
		body := makeEngineRequest(tc.mount, tc.op)
		req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
		req = withTokenInfo(req, &auth.TokenInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true})
		w := httptest.NewRecorder()
		srv.handleEngineRequest(w, req)

		// Admin passes the admin check; stub engine returns 200.
		if w.Code == http.StatusForbidden {
			t.Errorf("%s/%s: admin should not be forbidden, got 403", tc.mount, tc.op)
		}
	}
}

// TestEngineRequestUserRotateKeyOnUserMount verifies that a non-admin user
// can call rotate-key on a user engine mount (not blocked by transit's admin gate).
func TestEngineRequestUserRotateKeyOnUserMount(t *testing.T) {
	srv, sealMgr, _ := setupTestServer(t)
	unsealServer(t, sealMgr, nil)

	mountStubEngine(t, srv, "user-mount", engine.EngineTypeUser)

	// Create a policy rule allowing user operations.
	ctx := context.Background()
	_ = srv.policy.CreateRule(ctx, &policy.Rule{
		ID:        "allow-user-ops",
		Priority:  100,
		Effect:    policy.EffectAllow,
		Roles:     []string{"user"},
		Resources: []string{"engine/*/*"},
		Actions:   []string{"any"},
	})

	body := makeEngineRequest("user-mount", "rotate-key")
	req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
	req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
	w := httptest.NewRecorder()
	srv.handleEngineRequest(w, req)

	// rotate-key on a user mount should NOT be blocked as admin-only.
	if w.Code == http.StatusForbidden {
		t.Errorf("user rotate-key on user mount should not be forbidden, got 403: %s", w.Body.String())
	}
}

// TestEngineRequestUserRotateKeyOnTransitMount verifies that a non-admin user
// is blocked from calling rotate-key on a transit engine mount.
func TestEngineRequestUserRotateKeyOnTransitMount(t *testing.T) {
	srv, sealMgr, _ := setupTestServer(t)
	unsealServer(t, sealMgr, nil)

	mountStubEngine(t, srv, "transit-mount", engine.EngineTypeTransit)

	body := makeEngineRequest("transit-mount", "rotate-key")
	req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
	req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
	w := httptest.NewRecorder()
	srv.handleEngineRequest(w, req)

	if w.Code != http.StatusForbidden {
		t.Errorf("user rotate-key on transit mount should be 403, got %d", w.Code)
	}
}

// TestOperationAction verifies the action classification of operations.
func TestOperationAction(t *testing.T) {
	tests := map[string]string{
		// Read operations.
		"list-issuers":  policy.ActionRead,
		"list-certs":    policy.ActionRead,
		"get-cert":      policy.ActionRead,
		"get-root":      policy.ActionRead,
		"get-chain":     policy.ActionRead,
		"get-issuer":    policy.ActionRead,
		"list-keys":     policy.ActionRead,
		"get-key":       policy.ActionRead,
		"get-public-key": policy.ActionRead,
		"list-users":    policy.ActionRead,
		"get-profile":   policy.ActionRead,
		"list-profiles": policy.ActionRead,

		// Granular crypto operations (including batch variants).
		"encrypt":       policy.ActionEncrypt,
		"batch-encrypt": policy.ActionEncrypt,
		"decrypt":       policy.ActionDecrypt,
		"batch-decrypt": policy.ActionDecrypt,
		"sign":      policy.ActionSign,
		"sign-host": policy.ActionSign,
		"sign-user": policy.ActionSign,
		"verify":    policy.ActionVerify,
		"hmac":      policy.ActionHMAC,

		// Write operations.
		"issue":          policy.ActionWrite,
		"renew":          policy.ActionWrite,
		"create-issuer":  policy.ActionWrite,
		"delete-issuer":  policy.ActionWrite,
		"sign-csr":       policy.ActionWrite,
		"revoke":         policy.ActionWrite,
	}
	for op, want := range tests {
		if got := operationAction(op); got != want {
			t.Errorf("operationAction(%q) = %q, want %q", op, got, want)
		}
	}
}

func TestTokenInfoFromContext(t *testing.T) {
	ctx := context.Background()
	if info := TokenInfoFromContext(ctx); info != nil {
		t.Error("expected nil from empty context")
	}

	info := &auth.TokenInfo{Username: "test", IsAdmin: true}
	ctx = context.WithValue(ctx, tokenInfoKey, info)
	got := TokenInfoFromContext(ctx)
	if got == nil || got.Username != "test" {
		t.Error("expected token info from context")
	}
}
