package store

import (
	"context"
	"path/filepath"
	"testing"
)

// openTestStore opens an isolated file-backed db in a t.TempDir.
// In-memory + shared-cache works too but file makes failures easier
// to inspect when a test panics.
func openTestStore(t *testing.T) *Store {
	t.Helper()
	dir := t.TempDir()
	s, err := Open(context.Background(), filepath.Join(dir, "rm.db"))
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	t.Cleanup(func() { _ = s.Close() })
	return s
}

func TestOpenAppliesMigrations(t *testing.T) {
	t.Parallel()
	s := openTestStore(t)

	row := s.DB().QueryRow(`SELECT MAX(version) FROM schema_version`)
	var v int
	if err := row.Scan(&v); err != nil {
		t.Fatalf("scan: %v", err)
	}
	if v < 1 {
		t.Fatalf("expected at least migration 1 applied, got %d", v)
	}

	// Spot-check a few tables exist with expected columns.
	tables := []string{
		"users", "sessions", "hosts", "repos",
		"credentials", "schedules", "jobs", "job_logs",
		"snapshots", "alerts", "audit_log",
		"enrollment_tokens", "host_schedule_version",
	}
	for _, tbl := range tables {
		row := s.DB().QueryRow(
			`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`, tbl)
		var got string
		if err := row.Scan(&got); err != nil {
			t.Errorf("table %q missing: %v", tbl, err)
		}
	}
}

func TestMigrateIsIdempotent(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	path := filepath.Join(dir, "rm.db")

	for i := 0; i < 3; i++ {
		s, err := Open(context.Background(), path)
		if err != nil {
			t.Fatalf("open #%d: %v", i, err)
		}
		_ = s.Close()
	}

	s, err := Open(context.Background(), path)
	if err != nil {
		t.Fatalf("final open: %v", err)
	}
	defer s.Close()

	row := s.DB().QueryRow(`SELECT COUNT(*) FROM schema_version`)
	var n int
	if err := row.Scan(&n); err != nil {
		t.Fatalf("scan: %v", err)
	}
	migs, err := loadMigrations()
	if err != nil {
		t.Fatalf("load migrations: %v", err)
	}
	if n != len(migs) {
		t.Errorf("re-running migrations should not insert duplicate rows; want %d, got %d",
			len(migs), n)
	}
}

func TestMigration0009Schema(t *testing.T) {
	t.Parallel()
	s := openTestStore(t)
	ctx := context.Background()

	// host_credentials must have a composite PK (host_id, kind).
	// We verify this by inserting two rows for the same host_id (different kinds)
	// and confirming a duplicate (host_id, kind) fails.
	_, err := s.DB().ExecContext(ctx,
		`INSERT INTO hosts (id, name, os, arch, enrolled_at) VALUES (?,?,?,?,?)`,
		"h-0009", "test-host", "linux", "amd64", "2026-01-01T00:00:00Z")
	if err != nil {
		t.Fatalf("insert host: %v", err)
	}
	now := "2026-01-01T00:00:00Z"
	if _, err := s.DB().ExecContext(ctx,
		`INSERT INTO host_credentials (host_id, kind, enc_repo_creds, updated_at) VALUES (?,?,?,?)`,
		"h-0009", "repo", "enc-repo", now); err != nil {
		t.Fatalf("insert repo creds: %v", err)
	}
	if _, err := s.DB().ExecContext(ctx,
		`INSERT INTO host_credentials (host_id, kind, enc_repo_creds, updated_at) VALUES (?,?,?,?)`,
		"h-0009", "admin", "enc-admin", now); err != nil {
		t.Fatalf("insert admin creds: %v", err)
	}
	// Duplicate (host_id, kind) must fail.
	if _, err := s.DB().ExecContext(ctx,
		`INSERT INTO host_credentials (host_id, kind, enc_repo_creds, updated_at) VALUES (?,?,?,?)`,
		"h-0009", "repo", "enc-repo-2", now); err == nil {
		t.Fatal("expected unique constraint violation on (host_id, kind), got nil")
	}

	// CHECK (kind IN ('repo','admin')) must reject an invalid kind.
	if _, err := s.DB().ExecContext(ctx,
		`INSERT INTO host_credentials (host_id, kind, enc_repo_creds, updated_at) VALUES (?,?,?,?)`,
		"h-0009", "other", "enc-other", now); err == nil {
		t.Fatal("expected CHECK constraint violation on kind='other', got nil")
	}

	// host_repo_stats table must exist with expected columns.
	if _, err := s.DB().ExecContext(ctx,
		`INSERT INTO host_repo_stats (host_id, lock_present, updated_at) VALUES (?,?,?)`,
		"h-0009", 0, now); err != nil {
		t.Fatalf("insert host_repo_stats: %v", err)
	}
	var lockPresent int
	if err := s.DB().QueryRowContext(ctx,
		`SELECT lock_present FROM host_repo_stats WHERE host_id = ?`, "h-0009",
	).Scan(&lockPresent); err != nil {
		t.Fatalf("select host_repo_stats: %v", err)
	}
	if lockPresent != 0 {
		t.Errorf("expected lock_present=0, got %d", lockPresent)
	}

	// CHECK (last_check_status IN ('ok','errors_found','failed')) must reject
	// an invalid value.
	if _, err := s.DB().ExecContext(ctx,
		`UPDATE host_repo_stats SET last_check_status = ? WHERE host_id = ?`,
		"wat", "h-0009"); err == nil {
		t.Fatal("expected CHECK constraint violation on last_check_status='wat', got nil")
	}
}

func TestForeignKeysEnforced(t *testing.T) {
	t.Parallel()
	s := openTestStore(t)

	// Inserting a session with a non-existent user should fail because
	// FKs are on. Without the pragma, SQLite silently accepts this.
	_, err := s.DB().Exec(
		`INSERT INTO sessions (id, user_id, created_at, expires_at)
		 VALUES (?, ?, datetime('now'), datetime('now','+1 hour'))`,
		"sess1", "no-such-user")
	if err == nil {
		t.Fatal("expected FK violation, got nil")
	}
}
