package sqlite

import (
	"context"
	"database/sql"
	"strings"
	"testing"

	"github.com/steveyegge/beads/internal/storage/sqlite/migrations"
	"github.com/steveyegge/beads/internal/types"
)

func TestMigrateDirtyIssuesTable(t *testing.T) {
	t.Run("creates dirty_issues table if not exists", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db

		// Drop table if exists
		_, _ = db.Exec("DROP TABLE IF EXISTS dirty_issues")

		// Run migration
		if err := migrations.MigrateDirtyIssuesTable(db); err != nil {
			t.Fatalf("failed to migrate dirty_issues table: %v", err)
		}

		// Verify table exists
		var tableName string
		err := db.QueryRow(`
			SELECT name FROM sqlite_master
			WHERE type='table' AND name='dirty_issues'
		`).Scan(&tableName)
		if err != nil {
			t.Fatalf("dirty_issues table not found: %v", err)
		}
	})

	t.Run("adds content_hash column to existing table", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db

		// Drop and create table without content_hash
		_, _ = db.Exec("DROP TABLE IF EXISTS dirty_issues")
		_, err := db.Exec(`
			CREATE TABLE dirty_issues (
				issue_id TEXT PRIMARY KEY,
				marked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
			)
		`)
		if err != nil {
			t.Fatalf("failed to create table: %v", err)
		}

		// Run migration
		if err := migrations.MigrateDirtyIssuesTable(db); err != nil {
			t.Fatalf("failed to migrate dirty_issues table: %v", err)
		}

		// Verify content_hash column exists
		var hasContentHash bool
		err = db.QueryRow(`
			SELECT COUNT(*) > 0 FROM pragma_table_info('dirty_issues')
			WHERE name = 'content_hash'
		`).Scan(&hasContentHash)
		if err != nil {
			t.Fatalf("failed to check for content_hash column: %v", err)
		}
		if !hasContentHash {
			t.Error("content_hash column was not added")
		}
	})
}

func TestMigrateExternalRefColumn(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()
	db := store.db

	// Run migration
	if err := migrations.MigrateExternalRefColumn(db); err != nil {
		t.Fatalf("failed to migrate external_ref column: %v", err)
	}

	// Verify column exists
	rows, err := db.Query("PRAGMA table_info(issues)")
	if err != nil {
		t.Fatalf("failed to query table info: %v", err)
	}
	defer rows.Close()

	found := false
	for rows.Next() {
		var cid int
		var name, typ string
		var notnull, pk int
		var dflt *string
		if err := rows.Scan(&cid, &name, &typ, &notnull, &dflt, &pk); err != nil {
			t.Fatalf("failed to scan column info: %v", err)
		}
		if name == "external_ref" {
			found = true
			break
		}
	}

	if !found {
		t.Error("external_ref column was not added")
	}
}

func TestMigrateCompositeIndexes(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()
	db := store.db

	// Drop index if exists
	_, _ = db.Exec("DROP INDEX IF EXISTS idx_dependencies_depends_on_type")

	// Run migration
	if err := migrations.MigrateCompositeIndexes(db); err != nil {
		t.Fatalf("failed to migrate composite indexes: %v", err)
	}

	// Verify index exists
	var indexName string
	err := db.QueryRow(`
		SELECT name FROM sqlite_master
		WHERE type='index' AND name='idx_dependencies_depends_on_type'
	`).Scan(&indexName)
	if err != nil {
		t.Fatalf("composite index not found: %v", err)
	}
}

func TestMigrateClosedAtConstraint(t *testing.T) {
	s, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// The constraint is now enforced in schema, so we can't easily create inconsistent state.
	// Instead, just verify the migration runs successfully on clean data.
	issue := &types.Issue{Title: "test-migrate", Priority: 1, IssueType: "task", Status: "open"}
	err := s.CreateIssue(ctx, issue, "test")
	if err != nil {
		t.Fatalf("failed to create issue: %v", err)
	}

	// Run migration (should succeed with no inconsistent data)
	if err := migrations.MigrateClosedAtConstraint(s.db); err != nil {
		t.Fatalf("failed to migrate closed_at constraint: %v", err)
	}

	// Verify issue is still valid
	got, err := s.GetIssue(ctx, issue.ID)
	if err != nil {
		t.Fatalf("failed to get issue: %v", err)
	}
	if got.ClosedAt != nil {
		t.Error("closed_at should be nil for open issue")
	}
}

func TestMigrateCompactionColumns(t *testing.T) {
	s, cleanup := setupTestDB(t)
	defer cleanup()

	// Remove compaction columns if they exist
	_, _ = s.db.Exec(`ALTER TABLE issues DROP COLUMN compaction_level`)
	_, _ = s.db.Exec(`ALTER TABLE issues DROP COLUMN compacted_at`)
	_, _ = s.db.Exec(`ALTER TABLE issues DROP COLUMN original_size`)

	// Run migration (will fail since columns don't exist, but that's okay for this test)
	// The migration should handle this gracefully
	_ = migrations.MigrateCompactionColumns(s.db)

	// Verify at least one column exists by querying
	var exists bool
	err := s.db.QueryRow(`
		SELECT COUNT(*) > 0
		FROM pragma_table_info('issues')
		WHERE name = 'compaction_level'
	`).Scan(&exists)
	if err != nil {
		t.Fatalf("failed to check compaction columns: %v", err)
	}

	// It's okay if the columns don't exist in test schema
	// The migration is idempotent and will add them when needed
}

func TestMigrateSnapshotsTable(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()
	db := store.db

	// Drop table if exists
	_, _ = db.Exec("DROP TABLE IF EXISTS issue_snapshots")

	// Run migration
	if err := migrations.MigrateSnapshotsTable(db); err != nil {
		t.Fatalf("failed to migrate snapshots table: %v", err)
	}

	// Verify table exists
	var tableExists bool
	err := db.QueryRow(`
		SELECT COUNT(*) > 0
		FROM sqlite_master
		WHERE type='table' AND name='issue_snapshots'
	`).Scan(&tableExists)
	if err != nil {
		t.Fatalf("failed to check snapshots table: %v", err)
	}
	if !tableExists {
		t.Error("issue_snapshots table was not created")
	}
}

func TestMigrateCompactionConfig(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()
	db := store.db

	// Clear config table
	_, _ = db.Exec("DELETE FROM config WHERE key LIKE 'compact%'")

	// Run migration
	if err := migrations.MigrateCompactionConfig(db); err != nil {
		t.Fatalf("failed to migrate compaction config: %v", err)
	}

	// Verify config values exist
	var value string
	err := db.QueryRow(`SELECT value FROM config WHERE key = 'compaction_enabled'`).Scan(&value)
	if err != nil {
		t.Fatalf("compaction config not found: %v", err)
	}
	if value != "false" {
		t.Errorf("expected compaction_enabled='false', got %q", value)
	}
}

func TestMigrateCompactedAtCommitColumn(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()
	db := store.db

	// Run migration
	if err := migrations.MigrateCompactedAtCommitColumn(db); err != nil {
		t.Fatalf("failed to migrate compacted_at_commit column: %v", err)
	}

	// Verify column exists
	var columnExists bool
	err := db.QueryRow(`
		SELECT COUNT(*) > 0
		FROM pragma_table_info('issues')
		WHERE name = 'compacted_at_commit'
	`).Scan(&columnExists)
	if err != nil {
		t.Fatalf("failed to check compacted_at_commit column: %v", err)
	}
	if !columnExists {
		t.Error("compacted_at_commit column was not added")
	}
}

func TestMigrateExportHashesTable(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()
	db := store.db

	// Drop table if exists
	_, _ = db.Exec("DROP TABLE IF EXISTS export_hashes")

	// Run migration
	if err := migrations.MigrateExportHashesTable(db); err != nil {
		t.Fatalf("failed to migrate export_hashes table: %v", err)
	}

	// Verify table exists
	var tableName string
	err := db.QueryRow(`
		SELECT name FROM sqlite_master
		WHERE type='table' AND name='export_hashes'
	`).Scan(&tableName)
	if err != nil {
		t.Fatalf("export_hashes table not found: %v", err)
	}
}

func TestMigrateExternalRefUnique(t *testing.T) {
	t.Run("creates unique index on external_ref", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db

		externalRef1 := "JIRA-1"
		externalRef2 := "JIRA-2"
		issue1 := types.Issue{ID: "bd-1", Title: "Issue 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, ExternalRef: &externalRef1}
		if err := store.CreateIssue(context.Background(), &issue1, "test"); err != nil {
			t.Fatalf("failed to create issue1: %v", err)
		}

		issue2 := types.Issue{ID: "bd-2", Title: "Issue 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, ExternalRef: &externalRef2}
		if err := store.CreateIssue(context.Background(), &issue2, "test"); err != nil {
			t.Fatalf("failed to create issue2: %v", err)
		}

		if err := migrations.MigrateExternalRefUnique(db); err != nil {
			t.Fatalf("failed to migrate external_ref unique constraint: %v", err)
		}

		issue3 := types.Issue{ID: "bd-3", Title: "Issue 3", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, ExternalRef: &externalRef1}
		err := store.CreateIssue(context.Background(), &issue3, "test")
		if err == nil {
			t.Error("Expected error when creating issue with duplicate external_ref, got nil")
		}
	})

	t.Run("fails if duplicates exist", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db

		_, err := db.Exec(`DROP INDEX IF EXISTS idx_issues_external_ref_unique`)
		if err != nil {
			t.Fatalf("failed to drop index: %v", err)
		}

		externalRef := "JIRA-1"
		issue1 := types.Issue{ID: "bd-100", Title: "Issue 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, ExternalRef: &externalRef}
		if err := store.CreateIssue(context.Background(), &issue1, "test"); err != nil {
			t.Fatalf("failed to create issue1: %v", err)
		}

		_, err = db.Exec(`INSERT INTO issues (id, title, status, priority, issue_type, external_ref, created_at, updated_at) VALUES (?, ?, 'open', 1, 'task', ?, datetime('now'), datetime('now'))`, "bd-101", "Issue 2", externalRef)
		if err != nil {
			t.Fatalf("failed to create duplicate: %v", err)
		}

		err = migrations.MigrateExternalRefUnique(db)
		if err == nil {
			t.Error("Expected migration to fail with duplicates present")
		}
		if !strings.Contains(err.Error(), "duplicate external_ref values") {
			t.Errorf("Expected error about duplicates, got: %v", err)
		}
	})
}

func TestMigrateRepoMtimesTable(t *testing.T) {
	t.Run("creates repo_mtimes table if not exists", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db

		// Drop table if exists
		_, _ = db.Exec("DROP TABLE IF EXISTS repo_mtimes")

		// Run migration
		if err := migrations.MigrateRepoMtimesTable(db); err != nil {
			t.Fatalf("failed to migrate repo_mtimes table: %v", err)
		}

		// Verify table exists
		var tableName string
		err := db.QueryRow(`
			SELECT name FROM sqlite_master
			WHERE type='table' AND name='repo_mtimes'
		`).Scan(&tableName)
		if err != nil {
			t.Fatalf("repo_mtimes table not found: %v", err)
		}
	})

	t.Run("is idempotent", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db

		// Run migration twice
		if err := migrations.MigrateRepoMtimesTable(db); err != nil {
			t.Fatalf("first migration failed: %v", err)
		}
		if err := migrations.MigrateRepoMtimesTable(db); err != nil {
			t.Fatalf("second migration failed: %v", err)
		}

		// Verify table still exists and is correct
		var tableName string
		err := db.QueryRow(`
			SELECT name FROM sqlite_master
			WHERE type='table' AND name='repo_mtimes'
		`).Scan(&tableName)
		if err != nil {
			t.Fatalf("repo_mtimes table not found after idempotent migration: %v", err)
		}
	})
}

func TestMigrateContentHashColumn(t *testing.T) {
	t.Run("adds content_hash column if missing", func(t *testing.T) {
		s, cleanup := setupTestDB(t)
		defer cleanup()

		// Run migration (should be idempotent)
		if err := migrations.MigrateContentHashColumn(s.db); err != nil {
			t.Fatalf("failed to migrate content_hash column: %v", err)
		}

		// Verify column exists
		var colName string
		err := s.db.QueryRow(`
			SELECT name FROM pragma_table_info('issues')
			WHERE name = 'content_hash'
		`).Scan(&colName)
		if err == sql.ErrNoRows {
			t.Error("content_hash column was not added")
		} else if err != nil {
			t.Fatalf("failed to check content_hash column: %v", err)
		}
	})

	t.Run("populates content_hash for existing issues", func(t *testing.T) {
		s, cleanup := setupTestDB(t)
		defer cleanup()

		ctx := context.Background()

		// Create a test issue
		issue := &types.Issue{Title: "test-hash", Priority: 1, IssueType: "task", Status: "open"}
		err := s.CreateIssue(ctx, issue, "test")
		if err != nil {
			t.Fatalf("failed to create issue: %v", err)
		}

		// Clear its content_hash directly in DB
		_, err = s.db.Exec(`UPDATE issues SET content_hash = NULL WHERE id = ?`, issue.ID)
		if err != nil {
			t.Fatalf("failed to clear content_hash: %v", err)
		}

		// Verify it's cleared
		var hash sql.NullString
		err = s.db.QueryRow(`SELECT content_hash FROM issues WHERE id = ?`, issue.ID).Scan(&hash)
		if err != nil {
			t.Fatalf("failed to verify cleared hash: %v", err)
		}
		if hash.Valid {
			t.Error("content_hash should be NULL before migration")
		}

		// Drop the column to simulate fresh migration
		_, err = s.db.Exec(`
			CREATE TABLE issues_backup AS SELECT * FROM issues;
			DROP TABLE issues;
			CREATE TABLE issues (
				id TEXT PRIMARY KEY,
				title TEXT NOT NULL,
				description TEXT NOT NULL DEFAULT '',
				design TEXT NOT NULL DEFAULT '',
				acceptance_criteria TEXT NOT NULL DEFAULT '',
				notes TEXT NOT NULL DEFAULT '',
				status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'blocked', 'closed', 'tombstone')),
				priority INTEGER NOT NULL,
				issue_type TEXT NOT NULL CHECK (issue_type IN ('bug', 'feature', 'task', 'epic', 'chore', 'message', 'agent', 'role')),
				assignee TEXT,
				estimated_minutes INTEGER,
				created_at DATETIME NOT NULL,
				created_by TEXT DEFAULT '',
				updated_at DATETIME NOT NULL,
				closed_at DATETIME,
				external_ref TEXT,
				compaction_level INTEGER DEFAULT 0,
				compacted_at DATETIME,
				original_size INTEGER,
				compacted_at_commit TEXT,
				source_repo TEXT DEFAULT '.',
				close_reason TEXT DEFAULT '',
				deleted_at TEXT,
				deleted_by TEXT DEFAULT '',
				delete_reason TEXT DEFAULT '',
				original_type TEXT DEFAULT '',
				sender TEXT DEFAULT '',
				ephemeral INTEGER DEFAULT 0,
				pinned INTEGER DEFAULT 0,
				is_template INTEGER DEFAULT 0,
				replies_to TEXT DEFAULT '',
				relates_to TEXT DEFAULT '',
				duplicate_of TEXT DEFAULT '',
				superseded_by TEXT DEFAULT '',
				await_type TEXT DEFAULT '',
				await_id TEXT DEFAULT '',
				timeout_ns INTEGER DEFAULT 0,
				waiters TEXT DEFAULT '',
				hook_bead TEXT DEFAULT '',
				role_bead TEXT DEFAULT '',
				agent_state TEXT DEFAULT '',
				last_activity DATETIME,
				role_type TEXT DEFAULT '',
				rig TEXT DEFAULT '',
				CHECK ((status = 'closed') = (closed_at IS NOT NULL))
			);
			INSERT INTO issues SELECT id, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, created_at, '', updated_at, closed_at, external_ref, compaction_level, compacted_at, original_size, compacted_at_commit, source_repo, '', NULL, '', '', '', '', 0, 0, 0, '', '', '', '', '', '', 0, '', '', '', '', NULL, '', '' FROM issues_backup;
			DROP TABLE issues_backup;
		`)
		if err != nil {
			t.Fatalf("failed to drop content_hash column: %v", err)
		}

		// Run migration - this should add the column and populate it
		if err := migrations.MigrateContentHashColumn(s.db); err != nil {
			t.Fatalf("failed to migrate content_hash column: %v", err)
		}

		// Verify content_hash is now populated
		got, err := s.GetIssue(ctx, issue.ID)
		if err != nil {
			t.Fatalf("failed to get issue: %v", err)
		}
		if got.ContentHash == "" {
			t.Error("content_hash should be populated after migration")
		}
	})
}

func TestMigrateOrphanDetection(t *testing.T) {
	t.Run("detects orphaned child issues", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db
		ctx := context.Background()

		// Create a parent issue
		parent := &types.Issue{
			ID:        "bd-parent",
			Title:     "Parent Issue",
			Priority:  1,
			IssueType: "task",
			Status:    "open",
		}
		if err := store.CreateIssue(ctx, parent, "test"); err != nil {
			t.Fatalf("failed to create parent issue: %v", err)
		}

		// Create a valid child issue
		validChild := &types.Issue{
			ID:        "bd-parent.1",
			Title:     "Valid Child",
			Priority:  1,
			IssueType: "task",
			Status:    "open",
		}
		if err := store.CreateIssue(ctx, validChild, "test"); err != nil {
			t.Fatalf("failed to create valid child: %v", err)
		}

		// Create an orphaned child by directly inserting it into the database
		// (bypassing CreateIssue validation which checks for parent existence)
		_, err := db.Exec(`
			INSERT INTO issues (id, title, status, priority, issue_type, created_at, updated_at)
			VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
		`, "bd-missing.1", "Orphaned Child", "open", 1, "task")
		if err != nil {
			t.Fatalf("failed to create orphan: %v", err)
		}

		// Run migration - it should detect the orphan and log it
		if err := migrations.MigrateOrphanDetection(db); err != nil {
			t.Fatalf("failed to run orphan detection migration: %v", err)
		}

		// Verify the orphan still exists (migration doesn't delete)
		got, err := store.GetIssue(ctx, "bd-missing.1")
		if err != nil {
			t.Fatalf("orphan should still exist after migration: %v", err)
		}
		if got.ID != "bd-missing.1" {
			t.Errorf("expected orphan ID bd-missing.1, got %s", got.ID)
		}

		// Verify valid child still exists
		got, err = store.GetIssue(ctx, "bd-parent.1")
		if err != nil {
			t.Fatalf("valid child should still exist: %v", err)
		}
		if got.ID != "bd-parent.1" {
			t.Errorf("expected valid child ID bd-parent.1, got %s", got.ID)
		}
	})

	t.Run("no orphans found in clean database", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db
		ctx := context.Background()

		// Create a parent with valid children
		parent := &types.Issue{
			ID:        "bd-p1",
			Title:     "Parent",
			Priority:  1,
			IssueType: "task",
			Status:    "open",
		}
		if err := store.CreateIssue(ctx, parent, "test"); err != nil {
			t.Fatalf("failed to create parent: %v", err)
		}

		child := &types.Issue{
			ID:        "bd-p1.1",
			Title:     "Child",
			Priority:  1,
			IssueType: "task",
			Status:    "open",
		}
		if err := store.CreateIssue(ctx, child, "test"); err != nil {
			t.Fatalf("failed to create child: %v", err)
		}

		// Run migration - should succeed with no orphans
		if err := migrations.MigrateOrphanDetection(db); err != nil {
			t.Fatalf("migration should succeed with clean data: %v", err)
		}
	})

	t.Run("is idempotent", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db

		// Run migration multiple times
		for i := 0; i < 3; i++ {
			if err := migrations.MigrateOrphanDetection(db); err != nil {
				t.Fatalf("migration run %d failed: %v", i+1, err)
			}
		}
	})

	// GH#508: Verify that prefixes with dots don't trigger false positives
	t.Run("prefix with dots is not flagged as orphan", func(t *testing.T) {
		store, cleanup := setupTestDB(t)
		defer cleanup()
		db := store.db

		// Override the prefix for this test
		_, err := db.Exec(`UPDATE config SET value = 'my.project' WHERE key = 'issue_prefix'`)
		if err != nil {
			t.Fatalf("failed to update prefix: %v", err)
		}

		// Insert issues with dotted prefix directly (bypassing prefix validation)
		testCases := []struct {
			id          string
			expectOrphan bool
		}{
			// These should NOT be flagged as orphans (dots in prefix)
			{"my.project-abc123", false},
			{"my.project-xyz789", false},
			{"com.example.app-issue1", false},

			// This SHOULD be flagged as orphan (hierarchical, parent doesn't exist)
			{"my.project-missing.1", true},
		}

		for _, tc := range testCases {
			_, err := db.Exec(`
				INSERT INTO issues (id, title, status, priority, issue_type, created_at, updated_at)
				VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
			`, tc.id, "Test Issue", "open", 1, "task")
			if err != nil {
				t.Fatalf("failed to insert %s: %v", tc.id, err)
			}
		}

		// Query for orphans using the same logic as the migration
		rows, err := db.Query(`
			SELECT id
			FROM issues
			WHERE
			  (id GLOB '*.[0-9]' OR id GLOB '*.[0-9][0-9]' OR id GLOB '*.[0-9][0-9][0-9]' OR id GLOB '*.[0-9][0-9][0-9][0-9]')
			  AND rtrim(rtrim(id, '0123456789'), '.') NOT IN (SELECT id FROM issues)
			ORDER BY id
		`)
		if err != nil {
			t.Fatalf("query failed: %v", err)
		}
		defer rows.Close()

		var orphans []string
		for rows.Next() {
			var id string
			if err := rows.Scan(&id); err != nil {
				t.Fatalf("scan failed: %v", err)
			}
			orphans = append(orphans, id)
		}

		// Verify only the expected orphan is detected
		if len(orphans) != 1 {
			t.Errorf("expected 1 orphan, got %d: %v", len(orphans), orphans)
		}
		if len(orphans) == 1 && orphans[0] != "my.project-missing.1" {
			t.Errorf("expected orphan 'my.project-missing.1', got %q", orphans[0])
		}

		// Verify non-hierarchical dotted IDs are NOT flagged
		for _, tc := range testCases {
			if !tc.expectOrphan {
				for _, orphan := range orphans {
					if orphan == tc.id {
						t.Errorf("false positive: %s was incorrectly flagged as orphan", tc.id)
					}
				}
			}
		}
	})
}
