package cli

import (
	"bytes"
	"database/sql"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"banger/internal/store"

	"github.com/spf13/cobra"
	_ "modernc.org/sqlite"
)

func TestNewBangerdCommandSubcommands(t *testing.T) {
	cmd := NewBangerdCommand()
	if cmd.Use != "bangerd" {
		t.Errorf("Use = %q, want bangerd", cmd.Use)
	}
	for _, flag := range []string{"system", "root-helper", "check-migrations"} {
		if cmd.Flag(flag) == nil {
			t.Errorf("flag %q missing", flag)
		}
	}
}

func TestLastID(t *testing.T) {
	tests := []struct {
		name string
		in   []int
		want int
	}{
		{"nil", nil, 0},
		{"empty", []int{}, 0},
		{"single", []int{7}, 7},
		{"sorted ascending", []int{1, 2, 3}, 3},
		{"unsorted, max in middle", []int{1, 99, 5}, 99},
		{"duplicates", []int{4, 4, 2, 4}, 4},
		{"negative ignored", []int{-3, -1, 0}, 0},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := lastID(tc.in); got != tc.want {
				t.Fatalf("lastID(%v) = %d, want %d", tc.in, got, tc.want)
			}
		})
	}
}

// stubExit replaces bangerdExit for the test and returns a pointer to
// the captured exit code (-1 = not called) and a restore func.
func stubExit(t *testing.T) *int {
	t.Helper()
	called := -1
	prev := bangerdExit
	bangerdExit = func(code int) { called = code }
	t.Cleanup(func() { bangerdExit = prev })
	return &called
}

// pointHomeAtTempDB sets XDG_STATE_HOME (and HOME, which Resolve falls
// back to) so that paths.Resolve().DBPath lands at <tmp>/banger/state.db.
// Returns the DB path.
func pointHomeAtTempDB(t *testing.T) string {
	t.Helper()
	tmp := t.TempDir()
	t.Setenv("HOME", tmp)
	t.Setenv("XDG_STATE_HOME", tmp)
	t.Setenv("XDG_CONFIG_HOME", tmp)
	t.Setenv("XDG_CACHE_HOME", tmp)
	t.Setenv("XDG_RUNTIME_DIR", tmp)
	dir := filepath.Join(tmp, "banger")
	if err := os.MkdirAll(dir, 0o700); err != nil {
		t.Fatalf("mkdir state dir: %v", err)
	}
	return filepath.Join(dir, "state.db")
}

func TestRunCheckMigrationsCompatible(t *testing.T) {
	dbPath := pointHomeAtTempDB(t)
	s, err := store.Open(dbPath)
	if err != nil {
		t.Fatalf("store.Open: %v", err)
	}
	_ = s.Close()

	exit := stubExit(t)
	cmd := &cobra.Command{}
	var out bytes.Buffer
	cmd.SetOut(&out)

	if err := runCheckMigrations(cmd, false); err != nil {
		t.Fatalf("runCheckMigrations: %v", err)
	}
	if *exit != -1 {
		t.Errorf("bangerdExit called with %d, want no call", *exit)
	}
	if !strings.HasPrefix(out.String(), "compatible:") {
		t.Errorf("stdout = %q, want prefix \"compatible:\"", out.String())
	}
}

func TestRunCheckMigrationsMigrationsNeeded(t *testing.T) {
	dbPath := pointHomeAtTempDB(t)
	// Hand-craft a DB that has schema_migrations with only the baseline
	// row — InspectSchemaState classifies this as "migrations needed".
	dsn := "file:" + dbPath + "?_pragma=foreign_keys(1)"
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		t.Fatalf("sql.Open: %v", err)
	}
	if _, err := db.Exec(`CREATE TABLE schema_migrations (id INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)`); err != nil {
		t.Fatalf("create table: %v", err)
	}
	if _, err := db.Exec(`INSERT INTO schema_migrations VALUES (1, 'baseline', '2026-01-01T00:00:00Z')`); err != nil {
		t.Fatalf("insert baseline: %v", err)
	}
	_ = db.Close()

	exit := stubExit(t)
	cmd := &cobra.Command{}
	var out bytes.Buffer
	cmd.SetOut(&out)

	if err := runCheckMigrations(cmd, false); err != nil {
		t.Fatalf("runCheckMigrations: %v", err)
	}
	if *exit != 1 {
		t.Errorf("bangerdExit called with %d, want 1", *exit)
	}
	if !strings.HasPrefix(out.String(), "migrations needed:") {
		t.Errorf("stdout = %q, want prefix \"migrations needed:\"", out.String())
	}
}

func TestRunCheckMigrationsIncompatible(t *testing.T) {
	dbPath := pointHomeAtTempDB(t)
	s, err := store.Open(dbPath)
	if err != nil {
		t.Fatalf("store.Open: %v", err)
	}
	_ = s.Close()

	// Inject an unknown migration id directly so the binary's known set
	// is a strict subset — InspectSchemaState classifies as incompatible.
	dsn := "file:" + dbPath
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		t.Fatalf("sql.Open: %v", err)
	}
	if _, err := db.Exec(`INSERT INTO schema_migrations VALUES (9999, 'from_the_future', '2030-01-01T00:00:00Z')`); err != nil {
		t.Fatalf("insert future row: %v", err)
	}
	_ = db.Close()

	exit := stubExit(t)
	cmd := &cobra.Command{}
	var out bytes.Buffer
	cmd.SetOut(&out)

	if err := runCheckMigrations(cmd, false); err != nil {
		t.Fatalf("runCheckMigrations: %v", err)
	}
	if *exit != 2 {
		t.Errorf("bangerdExit called with %d, want 2", *exit)
	}
	if !strings.HasPrefix(out.String(), "incompatible:") {
		t.Errorf("stdout = %q, want prefix \"incompatible:\"", out.String())
	}
}

func TestRunCheckMigrationsInspectError(t *testing.T) {
	// Point at a state dir with a non-DB file at state.db so Inspect
	// fails to open it. The function should wrap the error with the path.
	dbPath := pointHomeAtTempDB(t)
	if err := os.WriteFile(dbPath, []byte("not a sqlite file"), 0o600); err != nil {
		t.Fatalf("write garbage: %v", err)
	}

	stubExit(t)
	cmd := &cobra.Command{}
	var out bytes.Buffer
	cmd.SetOut(&out)

	err := runCheckMigrations(cmd, false)
	if err == nil {
		t.Fatal("runCheckMigrations: nil error, want wrapped inspect error")
	}
	if !strings.Contains(err.Error(), dbPath) {
		t.Errorf("error %q does not mention DB path %q", err.Error(), dbPath)
	}
}
