package main

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"fn-registry/functions/infra"
	"fn-registry/registry"
)

// fnBinDir holds the temp directory for the compiled fn binary.
// It is created by TestMain and cleaned up at test end.
var fnBinDir string
var fnBinPath string

// TestMain compiles the fn binary once before all tests.
func TestMain(m *testing.M) {
	var err error
	fnBinDir, err = os.MkdirTemp("", "fn-vault-test-*")
	if err != nil {
		fmt.Fprintf(os.Stderr, "create temp dir: %v\n", err)
		os.Exit(1)
	}
	defer os.RemoveAll(fnBinDir)

	fnBinPath = filepath.Join(fnBinDir, "fn")
	// Find registry root by walking up from current directory.
	regRoot, err := findRoot()
	if err != nil {
		fmt.Fprintf(os.Stderr, "find root: %v\n", err)
		os.Exit(1)
	}
	cmd := exec.Command("go", "build", "-tags", "fts5", "-o", fnBinPath, ".")
	cmd.Dir = filepath.Join(regRoot, "cmd", "fn")
	if out, errB := cmd.CombinedOutput(); errB != nil {
		fmt.Fprintf(os.Stderr, "build fn: %v\n%s\n", errB, out)
		os.Exit(1)
	}

	os.Exit(m.Run())
}

func findRoot() (string, error) {
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}
	for {
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
			return dir, nil
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return "", fmt.Errorf("could not find go.mod from %s", dir)
		}
		dir = parent
	}
}

func ensureFnBin(t *testing.T) string {
	t.Helper()
	return fnBinPath
}

// setupTestRegistry creates a minimal registry root with:
//   - registry.db (opened + migrations applied via registry.Open)
//   - a project with a vault declared in vault.yaml
//   - a vault directory with some test files
//   - a symlink from projects/test_proj/vaults/test_vault -> vault dir
//
// Returns (repoRoot, vaultDir).
func setupTestRegistry(t *testing.T) (string, string) {
	t.Helper()
	repoRoot := t.TempDir()

	// Create vault directory with files.
	vaultDir := filepath.Join(t.TempDir(), "test_vault")
	if err := os.MkdirAll(filepath.Join(vaultDir, "data", "raw"), 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(vaultDir, "data", "raw", "report.csv"),
		[]byte("name,value\nfoo,1"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(vaultDir, "data", "raw", "notes.md"),
		[]byte("# Notes\nsome text"), 0644); err != nil {
		t.Fatal(err)
	}

	// Create project directory structure.
	projDir := filepath.Join(repoRoot, "projects", "test_proj")
	vaultsDir := filepath.Join(projDir, "vaults")
	if err := os.MkdirAll(vaultsDir, 0755); err != nil {
		t.Fatal(err)
	}

	// Create vault.yaml.
	vaultYAML := "vaults:\n  - name: test_vault\n    description: Test vault for unit tests\n    path: " + vaultDir + "\n    tags: [test]\n"
	if err := os.WriteFile(filepath.Join(vaultsDir, "vault.yaml"), []byte(vaultYAML), 0644); err != nil {
		t.Fatal(err)
	}

	// Create project.md.
	projMD := "---\nname: test_proj\ndescription: Test project\ntags: [test]\n---\n"
	if err := os.WriteFile(filepath.Join(projDir, "project.md"), []byte(projMD), 0644); err != nil {
		t.Fatal(err)
	}

	// Open registry.db (creates schema + runs migrations).
	db, err := registry.Open(filepath.Join(repoRoot, "registry.db"))
	if err != nil {
		t.Fatalf("registry.Open: %v", err)
	}

	// Index so the vault is registered in registry.db.
	if _, err := registry.Index(db, repoRoot); err != nil {
		t.Fatalf("registry.Index: %v", err)
	}
	db.Close()

	return repoRoot, vaultDir
}

// runFn runs the fn binary in repoRoot with the given args.
func runFn(t *testing.T, repoRoot string, args ...string) (string, string, int) {
	t.Helper()
	bin := ensureFnBin(t)
	cmd := exec.Command(bin, args...)
	cmd.Dir = repoRoot
	var stdout, stderr strings.Builder
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	err := cmd.Run()
	code := 0
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			code = exitErr.ExitCode()
		} else {
			t.Logf("cmd error: %v", err)
		}
	}
	return stdout.String(), stderr.String(), code
}

// TestVaultList verifies that 'fn vault list' shows the indexed vault.
func TestVaultList(t *testing.T) {
	repoRoot, _ := setupTestRegistry(t)
	out, stderr, code := runFn(t, repoRoot, "vault", "list")
	if code != 0 {
		t.Fatalf("fn vault list exit %d\nstderr: %s", code, stderr)
	}
	if !strings.Contains(out, "test_vault") {
		t.Errorf("expected 'test_vault' in output, got:\n%s", out)
	}
}

// TestVaultIndex verifies that 'fn vault index <name>' runs without error.
func TestVaultIndex(t *testing.T) {
	repoRoot, _ := setupTestRegistry(t)
	out, stderr, code := runFn(t, repoRoot, "vault", "index", "test_vault")
	if code != 0 {
		t.Fatalf("fn vault index exit %d\nstderr: %s\nstdout: %s", code, stderr, out)
	}
	if !strings.Contains(out, "indexed") {
		t.Errorf("expected 'indexed' in output, got:\n%s", out)
	}
}

// TestVaultSearchJSON verifies that 'fn vault search --json' returns valid JSON array.
func TestVaultSearchJSON(t *testing.T) {
	repoRoot, vaultDir := setupTestRegistry(t)

	// First index the vault so there is something to search.
	if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 {
		t.Fatal("fn vault index failed")
	}

	// Seed some content into the vault index for the search to find.
	db, err := infra.VaultIndexOpen(vaultDir)
	if err != nil {
		t.Fatalf("VaultIndexOpen: %v", err)
	}
	// Update content_text for FTS search.
	db.Exec(`DELETE FROM files_fts WHERE rel_path = 'data/raw/report.csv'`)
	db.Exec(`INSERT INTO files_fts(rel_path, content_text) VALUES ('data/raw/report.csv', 'foo report data')`)
	db.Close()

	out, stderr, code := runFn(t, repoRoot, "vault", "search", "report", "--json", "--vault", "test_vault")
	if code != 0 {
		t.Fatalf("fn vault search exit %d\nstderr: %s", code, stderr)
	}

	var result []map[string]interface{}
	if err := json.Unmarshal([]byte(out), &result); err != nil {
		t.Fatalf("output is not valid JSON: %v\nraw: %s", err, out)
	}
	// Should be a JSON array (possibly empty if search finds nothing, but must be valid).
	t.Logf("search returned %d hits", len(result))
}

// TestVaultInfo verifies that 'fn vault info <name>' outputs vault stats.
func TestVaultInfo(t *testing.T) {
	repoRoot, _ := setupTestRegistry(t)

	// Index first.
	if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 {
		t.Fatal("fn vault index failed")
	}

	out, stderr, code := runFn(t, repoRoot, "vault", "info", "test_vault")
	if code != 0 {
		t.Fatalf("fn vault info exit %d\nstderr: %s", code, stderr)
	}
	if !strings.Contains(out, "test_vault") {
		t.Errorf("expected vault name in output, got:\n%s", out)
	}
	if !strings.Contains(out, "Files:") {
		t.Errorf("expected 'Files:' in output, got:\n%s", out)
	}
}

// TestFormatBytes verifies the formatBytes helper.
func TestFormatBytes(t *testing.T) {
	cases := []struct {
		input    int64
		expected string
	}{
		{500, "500 B"},
		{1024, "1.0 KB"},
		{1536, "1.5 KB"},
		{1048576, "1.0 MB"},
		{1073741824, "1.0 GB"},
	}
	for _, tc := range cases {
		got := formatBytes(tc.input)
		if got != tc.expected {
			t.Errorf("formatBytes(%d) = %q, want %q", tc.input, got, tc.expected)
		}
	}
}

// TestVaultLayoutEnsure verifies that 'fn vault layout-ensure --dry-run' works.
func TestVaultLayoutEnsure(t *testing.T) {
	repoRoot, _ := setupTestRegistry(t)
	out, stderr, code := runFn(t, repoRoot, "vault", "layout-ensure", "test_vault", "--dry-run")
	if code != 0 {
		t.Fatalf("fn vault layout-ensure exit %d\nstderr: %s\nstdout: %s", code, stderr, out)
	}
	if !strings.Contains(out, "test_vault") {
		t.Errorf("expected vault name in output, got:\n%s", out)
	}
}

// TestVaultAggregate verifies that 'fn vault aggregate' runs without error on a clean registry.
func TestVaultAggregate(t *testing.T) {
	repoRoot, _ := setupTestRegistry(t)

	// Index first so there is something to aggregate.
	if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 {
		t.Fatal("fn vault index failed")
	}

	_, stderr, code := runFn(t, repoRoot, "vault", "aggregate")
	if code != 0 {
		t.Fatalf("fn vault aggregate exit %d\nstderr: %s", code, stderr)
	}
}

// TestVaultDoctor verifies that 'fn vault doctor' runs and reports on vaults.
func TestVaultDoctor(t *testing.T) {
	repoRoot, _ := setupTestRegistry(t)
	out, stderr, code := runFn(t, repoRoot, "vault", "doctor")
	if code != 0 {
		t.Fatalf("fn vault doctor exit %d\nstderr: %s", code, stderr)
	}
	if !strings.Contains(out, "test_vault") {
		t.Errorf("expected 'test_vault' in doctor output, got:\n%s", out)
	}
}

// TestVaultDedupe verifies that 'fn vault dedupe' runs without error after indexing.
func TestVaultDedupe(t *testing.T) {
	repoRoot, _ := setupTestRegistry(t)

	if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 {
		t.Fatal("fn vault index failed")
	}

	out, stderr, code := runFn(t, repoRoot, "vault", "dedupe", "test_vault")
	if code != 0 {
		t.Fatalf("fn vault dedupe exit %d\nstderr: %s", code, stderr)
	}
	// Should say "No duplicates" or show a table — either is fine.
	_ = out
}

// TestVaultAuditDryRun verifies that 'fn vault audit --dry-run-layout --skip-profilers' works.
func TestVaultAuditDryRun(t *testing.T) {
	repoRoot, _ := setupTestRegistry(t)
	out, stderr, code := runFn(t, repoRoot, "vault", "audit", "test_vault",
		"--dry-run-layout", "--skip-profilers")
	// Exit 0 = fully ok; exit 4 = warnings (layout issues) — both acceptable here.
	if code != 0 && code != 4 {
		t.Fatalf("fn vault audit exit %d\nstderr: %s\nstdout: %s", code, stderr, out)
	}
	if !strings.Contains(out, "summary") {
		t.Errorf("expected 'summary' section in audit output, got:\n%s", out)
	}
}

// Suppress unused import for time.
var _ = time.Now
