package formula

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"os"
	"path/filepath"
	"testing"
)

// TestGetEmbeddedFormulas verifies embedded formulas can be read and hashed.
func TestGetEmbeddedFormulas(t *testing.T) {
	embedded, err := getEmbeddedFormulas()
	if err != nil {
		t.Fatalf("getEmbeddedFormulas() error: %v", err)
	}
	if len(embedded) == 0 {
		t.Error("should have embedded formulas")
	}

	// Verify at least one known formula exists
	if _, ok := embedded["mol-deacon-patrol.formula.toml"]; !ok {
		t.Error("should contain mol-deacon-patrol.formula.toml")
	}

	// Verify hashes are valid hex strings
	for name, hash := range embedded {
		if len(hash) != 64 {
			t.Errorf("%s hash has wrong length: %d", name, len(hash))
		}
	}
}

// TestProvisionFormulas_FreshInstall tests provisioning to an empty directory.
func TestProvisionFormulas_FreshInstall(t *testing.T) {
	tmpDir := t.TempDir()

	count, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}
	if count == 0 {
		t.Error("should have provisioned at least one formula")
	}

	// Verify formulas directory was created
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	if _, err := os.Stat(formulasDir); os.IsNotExist(err) {
		t.Error("formulas directory should exist")
	}

	// Verify .installed.json was created
	installedPath := filepath.Join(formulasDir, ".installed.json")
	if _, err := os.Stat(installedPath); os.IsNotExist(err) {
		t.Error(".installed.json should exist")
	}

	// Verify installed record contains the right checksums
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		t.Fatalf("loadInstalledRecord() error: %v", err)
	}
	if len(installed.Formulas) != count {
		t.Errorf("installed record has %d entries, want %d", len(installed.Formulas), count)
	}
}

// TestProvisionFormulas_SkipsExisting tests that existing files are not overwritten.
func TestProvisionFormulas_SkipsExisting(t *testing.T) {
	tmpDir := t.TempDir()

	// Create formulas directory with a custom formula
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	if err := os.MkdirAll(formulasDir, 0755); err != nil {
		t.Fatal(err)
	}

	customContent := []byte("# Custom user formula\nformula = \"mol-deacon-patrol\"\n")
	customPath := filepath.Join(formulasDir, "mol-deacon-patrol.formula.toml")
	if err := os.WriteFile(customPath, customContent, 0644); err != nil {
		t.Fatal(err)
	}

	// Provision formulas
	_, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Verify custom content was NOT overwritten
	content, err := os.ReadFile(customPath)
	if err != nil {
		t.Fatal(err)
	}
	if string(content) != string(customContent) {
		t.Error("existing formula should not have been overwritten")
	}
}

// TestCheckFormulaHealth_AllOK tests when all formulas are up to date.
func TestCheckFormulaHealth_AllOK(t *testing.T) {
	tmpDir := t.TempDir()

	// Provision fresh
	_, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Check health
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatalf("CheckFormulaHealth() error: %v", err)
	}

	if report.Outdated != 0 {
		t.Errorf("Outdated = %d, want 0", report.Outdated)
	}
	if report.Missing != 0 {
		t.Errorf("Missing = %d, want 0", report.Missing)
	}
	if report.Modified != 0 {
		t.Errorf("Modified = %d, want 0", report.Modified)
	}
	if report.OK == 0 {
		t.Error("OK should be > 0")
	}
}

// TestCheckFormulaHealth_UserModified tests detection of user-modified formulas.
func TestCheckFormulaHealth_UserModified(t *testing.T) {
	tmpDir := t.TempDir()

	// Provision fresh
	_, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Modify a formula
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	formulaPath := filepath.Join(formulasDir, "mol-deacon-patrol.formula.toml")
	modifiedContent := []byte("# User modified this\nformula = \"mol-deacon-patrol\"\nversion = 999\n")
	if err := os.WriteFile(formulaPath, modifiedContent, 0644); err != nil {
		t.Fatal(err)
	}

	// Check health
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatalf("CheckFormulaHealth() error: %v", err)
	}

	if report.Modified != 1 {
		t.Errorf("Modified = %d, want 1", report.Modified)
	}

	// Verify the specific formula is marked as modified
	found := false
	for _, f := range report.Formulas {
		if f.Name == "mol-deacon-patrol.formula.toml" {
			if f.Status != "modified" {
				t.Errorf("mol-deacon-patrol status = %q, want %q", f.Status, "modified")
			}
			found = true
			break
		}
	}
	if !found {
		t.Error("mol-deacon-patrol.formula.toml not found in report")
	}
}

// TestCheckFormulaHealth_Missing tests detection of deleted formulas.
func TestCheckFormulaHealth_Missing(t *testing.T) {
	tmpDir := t.TempDir()

	// Provision fresh
	_, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Delete a formula
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	formulaPath := filepath.Join(formulasDir, "mol-deacon-patrol.formula.toml")
	if err := os.Remove(formulaPath); err != nil {
		t.Fatal(err)
	}

	// Check health
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatalf("CheckFormulaHealth() error: %v", err)
	}

	if report.Missing != 1 {
		t.Errorf("Missing = %d, want 1", report.Missing)
	}
}

// TestCheckFormulaHealth_Outdated simulates an outdated formula.
func TestCheckFormulaHealth_Outdated(t *testing.T) {
	tmpDir := t.TempDir()

	// Provision fresh
	_, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Simulate "old" installed record by changing the installed hash for a formula
	// This mimics what happens when a new binary has updated formula content
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		t.Fatal(err)
	}

	embedded, err := getEmbeddedFormulas()
	if err != nil {
		t.Fatal(err)
	}

	// Pick a formula that exists
	var targetFormula string
	for name := range installed.Formulas {
		targetFormula = name
		break
	}
	if targetFormula == "" {
		t.Skip("no formulas installed")
	}

	// Write a file that simulates "old version" - content differs from embedded
	formulaPath := filepath.Join(formulasDir, targetFormula)
	oldContent := []byte("# Old version of formula\n")
	if err := os.WriteFile(formulaPath, oldContent, 0644); err != nil {
		t.Fatal(err)
	}

	// Update installed record to match the old content's hash
	hash := sha256.Sum256(oldContent)
	installed.Formulas[targetFormula] = hex.EncodeToString(hash[:])

	if err := saveInstalledRecord(formulasDir, installed); err != nil {
		t.Fatal(err)
	}

	// Now: file matches what we "installed" but differs from embedded = outdated
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatalf("CheckFormulaHealth() error: %v", err)
	}

	if report.Outdated != 1 {
		t.Errorf("Outdated = %d, want 1", report.Outdated)
	}

	// Verify the embedded hash is different from installed
	embeddedHash := embedded[targetFormula]
	if embeddedHash == installed.Formulas[targetFormula] {
		t.Error("embedded hash should differ from installed hash for this test")
	}
}

// TestUpdateFormulas_UpdatesOutdated tests that outdated formulas are updated.
func TestUpdateFormulas_UpdatesOutdated(t *testing.T) {
	tmpDir := t.TempDir()

	// Provision fresh
	_, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Simulate outdated formula
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		t.Fatal(err)
	}

	var targetFormula string
	for name := range installed.Formulas {
		targetFormula = name
		break
	}
	if targetFormula == "" {
		t.Skip("no formulas installed")
	}

	// Write old content
	formulaPath := filepath.Join(formulasDir, targetFormula)
	oldContent := []byte("# Old version\n")
	if err := os.WriteFile(formulaPath, oldContent, 0644); err != nil {
		t.Fatal(err)
	}

	// Update installed record with old content's hash
	hash := sha256.Sum256(oldContent)
	installed.Formulas[targetFormula] = hex.EncodeToString(hash[:])
	if err := saveInstalledRecord(formulasDir, installed); err != nil {
		t.Fatal(err)
	}

	// Run update
	updated, skipped, reinstalled, err := UpdateFormulas(tmpDir)
	if err != nil {
		t.Fatalf("UpdateFormulas() error: %v", err)
	}

	if updated != 1 {
		t.Errorf("updated = %d, want 1", updated)
	}
	if skipped != 0 {
		t.Errorf("skipped = %d, want 0", skipped)
	}
	if reinstalled != 0 {
		t.Errorf("reinstalled = %d, want 0", reinstalled)
	}

	// Verify file was updated
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatalf("CheckFormulaHealth() error: %v", err)
	}
	if report.Outdated != 0 {
		t.Errorf("after update, Outdated = %d, want 0", report.Outdated)
	}
}

// TestUpdateFormulas_SkipsModified tests that user-modified formulas are skipped.
func TestUpdateFormulas_SkipsModified(t *testing.T) {
	tmpDir := t.TempDir()

	// Provision fresh
	_, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Modify a formula (user customization)
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		t.Fatal(err)
	}

	var targetFormula string
	for name := range installed.Formulas {
		targetFormula = name
		break
	}
	if targetFormula == "" {
		t.Skip("no formulas installed")
	}

	// Write different content that doesn't match installed hash
	formulaPath := filepath.Join(formulasDir, targetFormula)
	modifiedContent := []byte("# User customized this formula\nformula = \"custom\"\n")
	if err := os.WriteFile(formulaPath, modifiedContent, 0644); err != nil {
		t.Fatal(err)
	}

	// Run update - should skip the modified formula
	_, skipped, _, err := UpdateFormulas(tmpDir)
	if err != nil {
		t.Fatalf("UpdateFormulas() error: %v", err)
	}

	if skipped != 1 {
		t.Errorf("skipped = %d, want 1", skipped)
	}

	// Verify file was NOT changed
	content, err := os.ReadFile(formulaPath)
	if err != nil {
		t.Fatal(err)
	}
	if string(content) != string(modifiedContent) {
		t.Error("modified formula should not have been changed")
	}
}

// TestUpdateFormulas_ReinstallsMissing tests that deleted formulas are reinstalled.
func TestUpdateFormulas_ReinstallsMissing(t *testing.T) {
	tmpDir := t.TempDir()

	// Provision fresh
	_, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Delete a formula
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		t.Fatal(err)
	}

	var targetFormula string
	for name := range installed.Formulas {
		targetFormula = name
		break
	}
	if targetFormula == "" {
		t.Skip("no formulas installed")
	}

	formulaPath := filepath.Join(formulasDir, targetFormula)
	if err := os.Remove(formulaPath); err != nil {
		t.Fatal(err)
	}

	// Run update
	_, _, reinstalled, err := UpdateFormulas(tmpDir)
	if err != nil {
		t.Fatalf("UpdateFormulas() error: %v", err)
	}

	if reinstalled != 1 {
		t.Errorf("reinstalled = %d, want 1", reinstalled)
	}

	// Verify file was restored
	if _, err := os.Stat(formulaPath); os.IsNotExist(err) {
		t.Error("missing formula should have been reinstalled")
	}
}

// TestUpdateFormulas_InstallsNew tests that new formulas are installed.
func TestUpdateFormulas_InstallsNew(t *testing.T) {
	tmpDir := t.TempDir()

	// Create directory structure but with empty installed record
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	if err := os.MkdirAll(formulasDir, 0755); err != nil {
		t.Fatal(err)
	}

	// Write empty installed record
	emptyInstalled := &InstalledRecord{Formulas: make(map[string]string)}
	if err := saveInstalledRecord(formulasDir, emptyInstalled); err != nil {
		t.Fatal(err)
	}

	// Run update - should install all formulas as "new"
	updated, skipped, reinstalled, err := UpdateFormulas(tmpDir)
	if err != nil {
		t.Fatalf("UpdateFormulas() error: %v", err)
	}

	// All formulas should be installed
	embedded, err := getEmbeddedFormulas()
	if err != nil {
		t.Fatal(err)
	}

	total := updated + reinstalled
	if total != len(embedded) {
		t.Errorf("total installed = %d, want %d", total, len(embedded))
	}
	if skipped != 0 {
		t.Errorf("skipped = %d, want 0", skipped)
	}
}

// TestInstalledRecordPersistence tests that the installed record survives across operations.
func TestInstalledRecordPersistence(t *testing.T) {
	tmpDir := t.TempDir()

	// Provision
	count, err := ProvisionFormulas(tmpDir)
	if err != nil {
		t.Fatalf("ProvisionFormulas() error: %v", err)
	}

	// Load and verify
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		t.Fatal(err)
	}

	if len(installed.Formulas) != count {
		t.Errorf("installed has %d formulas, want %d", len(installed.Formulas), count)
	}

	// Verify file is valid JSON
	installedPath := filepath.Join(formulasDir, ".installed.json")
	data, err := os.ReadFile(installedPath)
	if err != nil {
		t.Fatal(err)
	}

	var decoded InstalledRecord
	if err := json.Unmarshal(data, &decoded); err != nil {
		t.Errorf("installed.json is not valid JSON: %v", err)
	}
}

// TestCheckFormulaHealth_NewFormula tests detection of new formulas that were never installed.
func TestCheckFormulaHealth_NewFormula(t *testing.T) {
	tmpDir := t.TempDir()

	// Create formulas directory with empty installed record
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	if err := os.MkdirAll(formulasDir, 0755); err != nil {
		t.Fatal(err)
	}

	// Write empty installed record - simulates pre-existing install without this formula
	emptyInstalled := &InstalledRecord{Formulas: make(map[string]string)}
	if err := saveInstalledRecord(formulasDir, emptyInstalled); err != nil {
		t.Fatal(err)
	}

	// Check health - all embedded formulas should be "new"
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatalf("CheckFormulaHealth() error: %v", err)
	}

	embedded, _ := getEmbeddedFormulas()
	if report.New != len(embedded) {
		t.Errorf("New = %d, want %d", report.New, len(embedded))
	}
	if report.OK != 0 {
		t.Errorf("OK = %d, want 0", report.OK)
	}
}

// TestCheckFormulaHealth_Untracked tests detection of files that exist but aren't
// in .installed.json and don't match embedded (e.g., from older gt version).
func TestCheckFormulaHealth_Untracked(t *testing.T) {
	tmpDir := t.TempDir()

	// Get embedded formulas
	embedded, err := getEmbeddedFormulas()
	if err != nil {
		t.Fatal(err)
	}

	// Create formulas directory without .installed.json
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	if err := os.MkdirAll(formulasDir, 0755); err != nil {
		t.Fatal(err)
	}

	// Write formula files with different content (simulating older version)
	for name := range embedded {
		oldContent := []byte("# old version of " + name + "\n[molecule]\nid = \"test\"\n")
		if err := os.WriteFile(filepath.Join(formulasDir, name), oldContent, 0644); err != nil {
			t.Fatal(err)
		}
	}

	// Check health - all should be "untracked" (not "modified" since not tracked)
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatalf("CheckFormulaHealth() error: %v", err)
	}

	if report.Untracked != len(embedded) {
		t.Errorf("Untracked = %d, want %d", report.Untracked, len(embedded))
	}
	if report.Modified != 0 {
		t.Errorf("Modified = %d, want 0 (untracked files shouldn't be marked as modified)", report.Modified)
	}
	if report.OK != 0 {
		t.Errorf("OK = %d, want 0", report.OK)
	}

	// Verify all formulas have status "untracked"
	for _, f := range report.Formulas {
		if f.Status != "untracked" {
			t.Errorf("formula %s status = %q, want %q", f.Name, f.Status, "untracked")
		}
	}
}

// TestUpdateFormulas_UpdatesUntracked tests that untracked files get updated.
func TestUpdateFormulas_UpdatesUntracked(t *testing.T) {
	tmpDir := t.TempDir()

	// Get embedded formulas
	embedded, err := getEmbeddedFormulas()
	if err != nil {
		t.Fatal(err)
	}

	// Create formulas directory without .installed.json
	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	if err := os.MkdirAll(formulasDir, 0755); err != nil {
		t.Fatal(err)
	}

	// Write formula files with different content (simulating older version)
	for name := range embedded {
		oldContent := []byte("# old version of " + name + "\n[molecule]\nid = \"test\"\n")
		if err := os.WriteFile(filepath.Join(formulasDir, name), oldContent, 0644); err != nil {
			t.Fatal(err)
		}
	}

	// Run update - should update all untracked formulas
	updated, skipped, reinstalled, err := UpdateFormulas(tmpDir)
	if err != nil {
		t.Fatalf("UpdateFormulas() error: %v", err)
	}

	// All untracked files should be updated (counted as "updated", not "reinstalled")
	if updated != len(embedded) {
		t.Errorf("updated = %d, want %d", updated, len(embedded))
	}
	if skipped != 0 {
		t.Errorf("skipped = %d, want 0", skipped)
	}
	if reinstalled != 0 {
		t.Errorf("reinstalled = %d, want 0", reinstalled)
	}

	// Verify files now match embedded
	for name, expectedHash := range embedded {
		content, err := os.ReadFile(filepath.Join(formulasDir, name))
		if err != nil {
			t.Fatalf("reading %s: %v", name, err)
		}
		actualHash := computeHash(content)
		if actualHash != expectedHash {
			t.Errorf("%s hash mismatch after update", name)
		}
	}

	// Verify .installed.json was created with correct hashes
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		t.Fatal(err)
	}
	for name, expectedHash := range embedded {
		if installed.Formulas[name] != expectedHash {
			t.Errorf(".installed.json hash for %s = %q, want %q",
				name, installed.Formulas[name], expectedHash)
		}
	}

	// Re-run health check - should be all OK now
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatal(err)
	}
	if report.OK != len(embedded) {
		t.Errorf("after update, OK = %d, want %d", report.OK, len(embedded))
	}
	if report.Untracked != 0 {
		t.Errorf("after update, Untracked = %d, want 0", report.Untracked)
	}
}

// TestCheckFormulaHealth_MixedScenarios tests a mix of OK, untracked, and modified.
func TestCheckFormulaHealth_MixedScenarios(t *testing.T) {
	tmpDir := t.TempDir()

	// Get embedded formulas
	embedded, err := getEmbeddedFormulas()
	if err != nil {
		t.Fatal(err)
	}
	if len(embedded) < 3 {
		t.Skip("need at least 3 formulas for this test")
	}

	formulasDir := filepath.Join(tmpDir, ".beads", "formulas")
	if err := os.MkdirAll(formulasDir, 0755); err != nil {
		t.Fatal(err)
	}

	// Prepare installed record with only some formulas tracked
	installed := &InstalledRecord{Formulas: make(map[string]string)}

	i := 0
	var okFormula, untrackedFormula, modifiedFormula string
	for name := range embedded {
		switch i {
		case 0:
			// First formula: write matching content, track it -> should be OK
			okFormula = name
			content, _ := formulasFS.ReadFile("formulas/" + name)
			if err := os.WriteFile(filepath.Join(formulasDir, name), content, 0644); err != nil {
				t.Fatal(err)
			}
			installed.Formulas[name] = computeHash(content)

		case 1:
			// Second formula: write old content, don't track -> should be untracked
			untrackedFormula = name
			oldContent := []byte("# untracked old version\n[molecule]\nid = \"test\"\n")
			if err := os.WriteFile(filepath.Join(formulasDir, name), oldContent, 0644); err != nil {
				t.Fatal(err)
			}
			// Don't add to installed record

		case 2:
			// Third formula: write different content, track with original hash -> should be modified
			modifiedFormula = name
			originalContent, _ := formulasFS.ReadFile("formulas/" + name)
			originalHash := computeHash(originalContent)
			modifiedContent := []byte("# user modified version\n[molecule]\nid = \"custom\"\n")
			if err := os.WriteFile(filepath.Join(formulasDir, name), modifiedContent, 0644); err != nil {
				t.Fatal(err)
			}
			installed.Formulas[name] = originalHash // Track with original hash
		}
		i++
		if i >= 3 {
			break
		}
	}

	if err := saveInstalledRecord(formulasDir, installed); err != nil {
		t.Fatal(err)
	}

	// Check health
	report, err := CheckFormulaHealth(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	// Find status of each test formula
	statusMap := make(map[string]string)
	for _, f := range report.Formulas {
		statusMap[f.Name] = f.Status
	}

	if statusMap[okFormula] != "ok" {
		t.Errorf("formula %s status = %q, want %q", okFormula, statusMap[okFormula], "ok")
	}
	if statusMap[untrackedFormula] != "untracked" {
		t.Errorf("formula %s status = %q, want %q", untrackedFormula, statusMap[untrackedFormula], "untracked")
	}
	if statusMap[modifiedFormula] != "modified" {
		t.Errorf("formula %s status = %q, want %q", modifiedFormula, statusMap[modifiedFormula], "modified")
	}
}
