package api

// Integration tests — white-box (same package) so we can call NewRouter directly.
// Tests the three core user journeys:
//   1. Create login   → agents and humans store credentials
//   2. Read login     → round-trip encrypt/decrypt verification  
//   3. Use login      → URL match, TOTP generation, MCP tool calls
//
// Uses inou.com as the test credential — a real login flow.
// Run: go test ./api/... -v

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"embed"

	"github.com/johanj/vault1984/lib"
)

// --- test helpers ---

type tc struct {
	srv   *httptest.Server
	token string
	t     *testing.T
}

func newTestClient(t *testing.T) *tc {
	t.Helper()

	tmpDir := t.TempDir()
	cfg := &lib.Config{
		Port:       "0",
		DataDir:    tmpDir,
		Mode:       "self-hosted",
		SessionTTL: 86400,
	}

	// Create a DB with a fake credential so VaultMiddleware can derive vault key
	fakePubKey := make([]byte, 65)
	for i := range fakePubKey { fakePubKey[i] = byte(i + 1) }
	dbPath := tmpDir + "/01020304.db"
	db, err := lib.OpenDB(dbPath)
	if err != nil { t.Fatalf("opendb: %v", err) }
	if err := lib.MigrateDB(db); err != nil { t.Fatalf("migrate: %v", err) }
	// Store fake credential for vault key derivation
	lib.StoreWebAuthnCredential(db, &lib.WebAuthnCredential{
		CredID: lib.HexID(1), Name: "test", PublicKey: fakePubKey,
		CredentialID: []byte("test-raw-id"),
	})
	db.Close()

	var emptyFS embed.FS
	srv := httptest.NewServer(NewRouter(cfg, emptyFS, emptyFS))
	t.Cleanup(srv.Close)

	// Auth
	resp, _ := srv.Client().Post(srv.URL+"/api/auth/setup", "application/json", nil)
	var setup struct{ Token string `json:"token"` }
	json.NewDecoder(resp.Body).Decode(&setup)
	resp.Body.Close()

	return &tc{srv: srv, token: setup.Token, t: t}
}

func (c *tc) req(method, path string, body any) *http.Response {
	c.t.Helper()
	var r io.Reader
	if body != nil {
		b, _ := json.Marshal(body)
		r = bytes.NewReader(b)
	}
	req, _ := http.NewRequest(method, c.srv.URL+path, r)
	if body != nil { req.Header.Set("Content-Type", "application/json") }
	if c.token != "" { req.Header.Set("Authorization", "Bearer "+c.token) }
	resp, err := c.srv.Client().Do(req)
	if err != nil { c.t.Fatalf("req %s %s: %v", method, path, err) }
	return resp
}

func (c *tc) must(resp *http.Response, wantStatus int) map[string]any {
	c.t.Helper()
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != wantStatus {
		c.t.Fatalf("expected %d, got %d: %s", wantStatus, resp.StatusCode, body)
	}
	var out map[string]any
	json.Unmarshal(body, &out)
	return out
}

func inouEntry() map[string]any {
	return map[string]any{
		"title": "inou.com",
		"type":  "credential",
		"data": map[string]any{
			"title": "inou.com",
			"type":  "credential",
			"fields": []map[string]any{
				{"label": "username", "value": "test@inou.com", "kind": "text"},
				{"label": "password", "value": "TestPass!InouDev42", "kind": "password"},
				{"label": "api_key",  "value": "inou_sk_test_abc123", "kind": "text"},
			},
			"urls": []string{"https://inou.com", "https://app.inou.com"},
		},
	}
}

// --- Tests ---

func TestHealth(t *testing.T) {
	c := newTestClient(t)
	result := c.must(c.req("GET", "/health", nil), 200)
	if result["status"] != "ok" {
		t.Errorf("health status = %v", result["status"])
	}
}

func TestCreateLogin(t *testing.T) {
	c := newTestClient(t)
	result := c.must(c.req("POST", "/api/entries", inouEntry()), 201)
	id, _ := result["entry_id"].(string)
	if id == "" {
		t.Fatal("create returned no entry_id")
	}
	t.Logf("Created entry: %s", id)
}

func TestReadLogin_RoundTrip(t *testing.T) {
	c := newTestClient(t)

	// Create
	created := c.must(c.req("POST", "/api/entries", inouEntry()), 201)
	id := created["entry_id"].(string)

	// Read back
	result := c.must(c.req("GET", "/api/entries/"+id, nil), 200)

	data, _ := result["data"].(map[string]any)
	if data == nil { t.Fatal("read returned no data") }

	if data["title"] != "inou.com" {
		t.Errorf("title = %v", data["title"])
	}

	// Verify password survived encrypt/decrypt
	fields, _ := data["fields"].([]any)
	var pw string
	for _, f := range fields {
		fm := f.(map[string]any)
		if fm["label"] == "password" {
			pw, _ = fm["value"].(string)
		}
	}
	if pw != "TestPass!InouDev42" {
		t.Errorf("password round-trip failed: got %q", pw)
	}
	t.Logf("Round-trip OK — password decrypted correctly")
}

func TestURLMatch(t *testing.T) {
	c := newTestClient(t)
	c.must(c.req("POST", "/api/entries", inouEntry()), 201)

	// Extension calls this when you land on app.inou.com/login
	c.must(c.req("GET", "/api/ext/match?url=https://app.inou.com/login", nil), 200)

	// result is an array
	resp := c.req("GET", "/api/ext/match?url=https://app.inou.com/login", nil)
	defer resp.Body.Close()
	var matches []map[string]any
	json.NewDecoder(resp.Body).Decode(&matches)

	if len(matches) == 0 {
		t.Error("no URL match for app.inou.com — expected inou.com entry to match")
	}
	t.Logf("URL match: %d entries for app.inou.com", len(matches))
}

func TestTOTP_AgentGeneratesCode(t *testing.T) {
	c := newTestClient(t)

	entry := map[string]any{
		"title": "inou.com",
		"type":  "credential",
		"data": map[string]any{
			"title": "inou.com",
			"type":  "credential",
			"fields": []map[string]any{
				{"label": "username", "value": "test@inou.com", "kind": "text"},
				{"label": "password", "value": "pass", "kind": "password"},
				{"label": "totp",     "value": "JBSWY3DPEHPK3PXP", "kind": "totp", "l2": false},
			},
			"urls": []string{"https://inou.com"},
		},
	}
	created := c.must(c.req("POST", "/api/entries", entry), 201)
	id := created["entry_id"].(string)

	result := c.must(c.req("GET", "/api/ext/totp/"+id, nil), 200)
	code, _ := result["code"].(string)
	if len(code) != 6 {
		t.Errorf("TOTP code = %q, want 6 digits", code)
	}
	expiresIn, _ := result["expires_in"].(float64)
	if expiresIn <= 0 || expiresIn > 30 {
		t.Errorf("expires_in = %v", expiresIn)
	}
	t.Logf("TOTP: %s (expires in %.0fs) — agent can complete 2FA without human", code, expiresIn)
}

func TestMCP_ListCredentials(t *testing.T) {
	c := newTestClient(t)

	for _, title := range []string{"inou.com", "GitHub", "AWS"} {
		c.req("POST", "/api/entries", map[string]any{
			"title": title, "type": "credential",
			"data": map[string]any{
				"title": title, "type": "credential",
				"fields": []map[string]any{{"label": "password", "value": "x", "kind": "password"}},
			},
		})
	}

	mcp := map[string]any{
		"jsonrpc": "2.0", "id": 1,
		"method": "tools/call",
		"params": map[string]any{"name": "list_credentials", "arguments": map[string]any{}},
	}
	result := c.must(c.req("POST", "/mcp", mcp), 200)

	res, _ := result["result"].(map[string]any)
	if res == nil {
		t.Fatalf("MCP error: %v", result["error"])
	}
	content, _ := res["content"].([]any)
	if len(content) == 0 {
		t.Fatal("MCP list_credentials returned empty content")
	}
	text := content[0].(map[string]any)["text"].(string)
	t.Logf("MCP list_credentials: %s", text[:min(len(text), 120)])
}

func TestMCP_GetCredential_Inou(t *testing.T) {
	c := newTestClient(t)
	c.must(c.req("POST", "/api/entries", inouEntry()), 201)

	mcp := map[string]any{
		"jsonrpc": "2.0", "id": 2,
		"method": "tools/call",
		"params": map[string]any{"name": "get_credential", "arguments": map[string]any{"query": "inou.com"}},
	}
	result := c.must(c.req("POST", "/mcp", mcp), 200)

	res, _ := result["result"].(map[string]any)
	if res == nil { t.Fatalf("MCP error: %v", result["error"]) }
	content, _ := res["content"].([]any)
	if len(content) == 0 { t.Fatal("empty content") }

	text := content[0].(map[string]any)["text"].(string)
	if !strings.Contains(text, "inou_sk_test_abc123") {
		t.Errorf("API key missing from MCP response: %s", text)
	}
	t.Logf("MCP get_credential: agent retrieved inou.com API key")
}

func TestMCP_GetTOTP(t *testing.T) {
	c := newTestClient(t)

	entry := map[string]any{
		"title": "inou.com",
		"type":  "credential",
		"data": map[string]any{
			"title": "inou.com",
			"type":  "credential",
			"fields": []map[string]any{
				{"label": "totp", "value": "JBSWY3DPEHPK3PXP", "kind": "totp"},
			},
		},
	}
	c.must(c.req("POST", "/api/entries", entry), 201)

	mcp := map[string]any{
		"jsonrpc": "2.0", "id": 3,
		"method": "tools/call",
		"params": map[string]any{"name": "get_totp", "arguments": map[string]any{"query": "inou.com"}},
	}
	result := c.must(c.req("POST", "/mcp", mcp), 200)
	res, _ := result["result"].(map[string]any)
	if res == nil { t.Fatalf("MCP error: %v", result["error"]) }
	content, _ := res["content"].([]any)
	text := content[0].(map[string]any)["text"].(string)

	// MCP get_totp returns JSON: {"code":"XXXXXX","expires_in":N}
	var totpJSON struct {
		Code      string `json:"code"`
		ExpiresIn int    `json:"expires_in"`
	}
	if err := json.Unmarshal([]byte(text), &totpJSON); err != nil {
		t.Fatalf("get_totp response not valid JSON: %s", text)
	}
	if len(totpJSON.Code) != 6 {
		t.Errorf("TOTP code = %q, want 6 digits", totpJSON.Code)
	}
	if totpJSON.ExpiresIn <= 0 || totpJSON.ExpiresIn > 30 {
		t.Errorf("expires_in = %d", totpJSON.ExpiresIn)
	}
	t.Logf("MCP get_totp: %s (expires in %ds)", totpJSON.Code, totpJSON.ExpiresIn)
}

func TestScopedToken_HidesOtherEntries(t *testing.T) {
	c := newTestClient(t)

	// Two entries — we'll scope the token to only one of them
	inouResult := c.must(c.req("POST", "/api/entries", map[string]any{
		"title": "inou.com", "type": "credential",
		"data": map[string]any{
			"title": "inou.com", "type": "credential",
			"fields": []map[string]any{{"label": "key", "value": "inou_key", "kind": "text"}},
		},
	}), 201)
	inouID, _ := inouResult["entry_id"].(string)

	c.must(c.req("POST", "/api/entries", map[string]any{
		"title": "GitHub", "type": "credential",
		"data": map[string]any{
			"title": "GitHub", "type": "credential",
			"fields": []map[string]any{{"label": "token", "value": "ghp_secret", "kind": "password"}},
		},
	}), 201)

	// Scoped token: inou entry only
	tokenResult := c.must(c.req("POST", "/api/mcp-tokens", map[string]any{
		"label": "inou-agent", "entry_ids": []string{inouID}, "read_only": true,
	}), 201)
	scopedToken, _ := tokenResult["token"].(string)

	// Scoped tokens are MCP-only — test via MCP list_credentials
	scoped := &tc{srv: c.srv, token: scopedToken, t: t}
	mcp := map[string]any{
		"jsonrpc": "2.0", "id": 99,
		"method": "tools/call",
		"params": map[string]any{"name": "list_credentials", "arguments": map[string]any{"search": ""}},
	}
	result := scoped.must(scoped.req("POST", "/mcp", mcp), 200)
	res, _ := result["result"].(map[string]any)
	if res == nil { t.Fatalf("MCP error: %v", result["error"]) }
	contentList, _ := res["content"].([]any)
	text := ""
	if len(contentList) > 0 {
		text, _ = contentList[0].(map[string]any)["text"].(string)
	}
	if strings.Contains(text, "GitHub") {
		t.Errorf("scoped token leaked GitHub entry: %s", text)
	}
	t.Logf("Scoped token OK: only scoped entries visible")
}

func TestPasswordGenerator(t *testing.T) {
	c := newTestClient(t)
	result := c.must(c.req("GET", "/api/generate?length=24&type=random", nil), 200)
	pw, _ := result["password"].(string)
	if len(pw) < 16 {
		t.Errorf("password too short: %q", pw)
	}
	t.Logf("Generated: %s", pw)
}

func TestAuditLog(t *testing.T) {
	c := newTestClient(t)
	created := c.must(c.req("POST", "/api/entries", inouEntry()), 201)
	id := created["entry_id"].(string)
	c.req("GET", "/api/entries/"+id, nil)

	// Audit log returns a plain JSON array
	auditResp := c.req("GET", "/api/audit", nil)
	if auditResp.StatusCode != 200 {
		t.Fatalf("audit returned %d", auditResp.StatusCode)
	}
	defer auditResp.Body.Close()
	var events []map[string]any
	json.NewDecoder(auditResp.Body).Decode(&events)
	if len(events) == 0 {
		t.Error("audit log empty after create + read")
	}
	t.Logf("Audit log: %d events", len(events))
}

func min(a, b int) int { if a < b { return a }; return b }

// --- WebAuthn Auth Flow Tests ---

func TestAuthStatus_FreshVault(t *testing.T) {
	c := newTestClient(t)
	resp, _ := c.srv.Client().Get(c.srv.URL + "/api/auth/status")
	defer resp.Body.Close()
	var result map[string]any
	json.NewDecoder(resp.Body).Decode(&result)

	if result["state"] != "fresh" {
		t.Errorf("expected fresh state, got %v", result["state"])
	}
	if result["credentials"].(float64) != 0 {
		t.Errorf("expected 0 credentials, got %v", result["credentials"])
	}
	t.Logf("Auth status: %v", result)
}

func TestAuthStatus_LockedVault(t *testing.T) {
	c := newTestClient(t)
	// Register a credential via authenticated endpoint
	c.must(c.req("POST", "/api/webauthn/register/complete", map[string]any{
		"public_key": []byte{1, 2, 3},
		"prf_salt":   []byte{4, 5, 6},
		"name":       "Test Key",
	}), 201)

	// Check status (unauthenticated)
	resp, _ := c.srv.Client().Get(c.srv.URL + "/api/auth/status")
	defer resp.Body.Close()
	var result map[string]any
	json.NewDecoder(resp.Body).Decode(&result)

	if result["state"] != "locked" {
		t.Errorf("expected locked state, got %v", result["state"])
	}
	t.Logf("Auth status after registration: %v", result)
}

func TestAuthRegister_BlocksWhenCredentialsExist(t *testing.T) {
	c := newTestClient(t)
	// Register a credential
	c.must(c.req("POST", "/api/webauthn/register/complete", map[string]any{
		"public_key": []byte{1, 2, 3},
		"prf_salt":   []byte{4, 5, 6},
		"name":       "Test Key",
	}), 201)

	// Try unauthenticated registration (should be blocked)
	resp, _ := c.srv.Client().Post(c.srv.URL+"/api/auth/register/begin", "application/json", nil)
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusForbidden {
		t.Errorf("expected 403, got %d", resp.StatusCode)
	}
	t.Log("Unauthenticated register correctly blocked when credentials exist")
}

func TestSetup_CreatesSession(t *testing.T) {
	c := newTestClient(t)
	// Setup should create a session
	resp, _ := c.srv.Client().Post(c.srv.URL+"/api/auth/setup", "application/json", nil)
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200, got %d", resp.StatusCode)
	}
	t.Log("Setup creates session correctly")
}

func TestAuthRegisterBegin_FreshVault(t *testing.T) {
	c := newTestClient(t)
	resp, _ := c.srv.Client().Post(c.srv.URL+"/api/auth/register/begin", "application/json", strings.NewReader("{}"))
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	var options map[string]any
	json.NewDecoder(resp.Body).Decode(&options)
	pk := options["publicKey"].(map[string]any)
	if pk["challenge"] == nil {
		t.Fatal("no challenge in response")
	}
	t.Logf("Register begin: challenge generated, rp=%v", pk["rp"])
}
