package main

import (
	"encoding/json"
	"os"
	"path/filepath"
	"testing"
)

func packageRoot(t *testing.T) string {
	t.Helper()
	wd, e := os.Getwd()
	if e != nil {
		t.Fatal(e)
	}
	root := filepath.Dir(wd)
	if _, e := os.Stat(filepath.Join(root, "data")); e != nil {
		t.Skip("package data not beside Source_Code")
	}
	return root
}

func applyPlanForTest(t *testing.T, p *InstallPlan) {
	t.Helper()
	for _, w := range p.Writes {
		dest, e := safeJoin(p.DataRoot, w.Rel)
		if e != nil {
			t.Fatal(e)
		}
		mustWrite(t, dest, w.Data)
	}
}

func TestPackagedSourceAndDefaultConvergence(t *testing.T) {
	root := packageRoot(t)
	s, e := ValidateSource(root)
	if e != nil {
		t.Fatal(e)
	}
	target := filepath.Join(t.TempDir(), "data")
	if e = os.MkdirAll(target, 0755); e != nil {
		t.Fatal(e)
	}
	p, e := BuildInstallPlan(s, target, DefaultOptions())
	if e != nil {
		t.Fatal(e)
	}
	applyPlanForTest(t, p)
	for _, rel := range append(append([]string{}, sharedUI...), append([]string{worldBank}, dedicatedJSON...)...) {
		a, e := parseJSONFile(filepath.Join(target, filepath.FromSlash(rel)))
		if e != nil {
			t.Fatal(e)
		}
		b, e := parseJSONFile(filepath.Join(root, "data", filepath.FromSlash(rel)))
		if e != nil {
			t.Fatal(e)
		}
		aj, _ := json.Marshal(a)
		bj, _ := json.Marshal(b)
		if string(aj) != string(bj) {
			t.Fatalf("default semantic mismatch: %s", rel)
		}
	}
	for _, rel := range append(append([]string{}, assetFiles...), alphaTarget, rgbTarget) {
		a, e := os.ReadFile(filepath.Join(target, filepath.FromSlash(rel)))
		if e != nil {
			t.Fatal(e)
		}
		b, e := os.ReadFile(filepath.Join(root, "data", filepath.FromSlash(rel)))
		if e != nil {
			t.Fatal(e)
		}
		if hashBytes(a) != hashBytes(b) {
			t.Fatalf("default asset mismatch: %s", rel)
		}
	}
}

func TestOtherModPreservedAndVariantConverges(t *testing.T) {
	root := packageRoot(t)
	s, e := ValidateSource(root)
	if e != nil {
		t.Fatal(e)
	}
	target := filepath.Join(t.TempDir(), "data")
	if e = os.MkdirAll(target, 0755); e != nil {
		t.Fatal(e)
	}
	p, e := BuildInstallPlan(s, target, DefaultOptions())
	if e != nil {
		t.Fatal(e)
	}
	applyPlanForTest(t, p)
	invp := filepath.Join(target, filepath.FromSlash(sharedUI[0]))
	inv, e := parseJSONFile(invp)
	if e != nil {
		t.Fatal(e)
	}
	inv["OtherModField"] = "keep"
	a := objChildren(inv)
	a = append(a, map[string]any{"type": "X", "name": "OtherModChild"})
	setChildren(inv, a)
	bb, _ := jsonBytes(inv)
	mustWrite(t, invp, bb)
	wbp := filepath.Join(target, filepath.FromSlash(worldBank))
	wb, e := parseJSONFile(wbp)
	if e != nil {
		t.Fatal(e)
	}
	deps := wb["dependencies"].(map[string]any)
	deps["json"] = append(deps["json"].([]any), map[string]any{"path": "data/other_mod.json"})
	wb["entities"] = append(wb["entities"].([]any), map[string]any{"type": "Entity", "name": "OtherModEntity", "id": json.Number("9")})
	bb, _ = jsonBytes(wb)
	mustWrite(t, wbp, bb)
	opt := BeaconOptions{"c19", "s07", "o100"}
	p2, e := BuildInstallPlan(s, target, opt)
	if e != nil {
		t.Fatal(e)
	}
	applyPlanForTest(t, p2)
	inv, e = parseJSONFile(invp)
	if e != nil {
		t.Fatal(e)
	}
	if inv["OtherModField"] != "keep" || findNodeByName(inv, "OtherModChild") == nil {
		t.Fatal("other mod inventory data lost")
	}
	wb, e = parseJSONFile(wbp)
	if e != nil {
		t.Fatal(e)
	}
	foundDep, foundEnt := false, false
	for _, x := range wb["dependencies"].(map[string]any)["json"].([]any) {
		m, _ := x.(map[string]any)
		if m != nil && m["path"] == "data/other_mod.json" {
			foundDep = true
		}
	}
	for _, x := range wb["entities"].([]any) {
		m, _ := x.(map[string]any)
		if m != nil && m["name"] == "OtherModEntity" {
			foundEnt = true
		}
	}
	if !foundDep || !foundEnt {
		t.Fatal("other mod world data lost")
	}
	got, _ := hashFile(filepath.Join(target, filepath.FromSlash(alphaTarget)))
	want, _ := hashFile(filepath.Join(root, filepath.FromSlash(variantRel(opt, "$a"))))
	if got != want {
		t.Fatal("variant alpha did not converge")
	}
	p3, e := BuildInstallPlan(s, target, opt)
	if e != nil {
		t.Fatal(e)
	}
	applyPlanForTest(t, p3)
	inv, e = parseJSONFile(invp)
	if e != nil {
		t.Fatal(e)
	}
	count := 0
	for _, x := range objChildren(inv) {
		if childName(x) == "fake_stash_button" {
			count++
		}
	}
	if count != 1 {
		t.Fatalf("duplicate fake stash button: %d", count)
	}
}

func TestTargetChangeDetected(t *testing.T) {
	root := packageRoot(t)
	s, e := ValidateSource(root)
	if e != nil {
		t.Fatal(e)
	}
	target := filepath.Join(t.TempDir(), "data")
	os.MkdirAll(target, 0755)
	p, e := BuildInstallPlan(s, target, DefaultOptions())
	if e != nil {
		t.Fatal(e)
	}
	mustWrite(t, filepath.Join(target, filepath.FromSlash(sharedUI[0])), []byte("{}\n"))
	if e = VerifyTargetSnapshot(p); e == nil {
		t.Fatal("target change was not detected")
	}
}

func TestBackupRestoreWithoutSource(t *testing.T) {
	root := packageRoot(t)
	s, e := ValidateSource(root)
	if e != nil {
		t.Fatal(e)
	}
	target := filepath.Join(t.TempDir(), "data")
	os.MkdirAll(target, 0755)
	p, e := BuildInstallPlan(s, target, DefaultOptions())
	if e != nil {
		t.Fatal(e)
	}
	backupRoot := t.TempDir()
	folder, e := ExecuteInstall(backupRoot, p)
	if e != nil {
		t.Fatal(e)
	}
	marker := filepath.Join(target, filepath.FromSlash(sharedUI[0]))
	before, _ := os.ReadFile(marker)
	mustWrite(t, marker, []byte("{}\n"))
	if _, e = RestoreBackup(backupRoot, target, folder); e != nil {
		t.Fatal(e)
	}
	after, _ := os.ReadFile(marker)
	if len(before) != 0 && hashBytes(before) == hashBytes(after) {
		t.Fatal("expected first-install backup to remove originally absent file")
	}
	if _, e = os.Stat(marker); !os.IsNotExist(e) {
		t.Fatal("originally absent file was not removed by restore")
	}
}

func TestSourceSessionFingerprintMismatch(t *testing.T) {
	root := packageRoot(t)
	s, e := ValidateSource(root)
	if e != nil {
		t.Fatal(e)
	}
	for k := range s.Fingerprints {
		s.Fingerprints[k] = "bad"
		break
	}
	if e = VerifySourceSession(s); e == nil {
		t.Fatal("fingerprint mismatch not detected")
	}
}

func TestDedicatedConflictStopsPlan(t *testing.T) {
	root := packageRoot(t)
	s, e := ValidateSource(root)
	if e != nil {
		t.Fatal(e)
	}
	target := filepath.Join(t.TempDir(), "data")
	os.MkdirAll(filepath.Join(target, "global/ui/layouts"), 0755)
	mustWrite(t, filepath.Join(target, "global/ui/layouts/fakestashcontrolleroriginalactionhd.json"), []byte("{\"name\":\"OtherModPanel\"}\n"))
	if _, e = BuildInstallPlan(s, target, DefaultOptions()); e == nil {
		t.Fatal("dedicated conflict was not blocked")
	}
}

func TestInjectedInstallFailureRollsBack(t *testing.T) {
	root := packageRoot(t)
	s, e := ValidateSource(root)
	if e != nil {
		t.Fatal(e)
	}
	target := filepath.Join(t.TempDir(), "data")
	os.MkdirAll(target, 0755)
	p, e := BuildInstallPlan(s, target, DefaultOptions())
	if e != nil {
		t.Fatal(e)
	}
	applyPlanForTest(t, p)
	before, e := snapshot(target, allOutputPaths())
	if e != nil {
		t.Fatal(e)
	}
	p2, e := BuildInstallPlan(s, target, BeaconOptions{"c02", "s01", "o50"})
	if e != nil {
		t.Fatal(e)
	}
	backupRoot := t.TempDir()
	if _, e = executeInstall(backupRoot, p2, 1); e == nil {
		t.Fatal("injected failure did not fail")
	}
	after, e := snapshot(target, allOutputPaths())
	if e != nil {
		t.Fatal(e)
	}
	if !compareSnapshot(before, after) {
		t.Fatal("rollback was not byte-for-byte for affected files")
	}
}
