package main

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

func testBaseline() []byte {
	return []byte(`{
// comment
"type":"AnimationTimelines",
"otherMod":{"keep":123,},
"timelines":{
  "hth_death":{"type":"AnimationTimeline","name":"hth_death","animationEvents":[]},
  "foo":{"type":"Other","x":7,},
},
}`)
}

func TestParsePillarCode(t *testing.T) {
	for _, v := range []string{"gold2", " red1 ", "MAGENTA3"} {
		if _, e := ParsePillarCode(v); e != nil {
			t.Fatal(e)
		}
	}
	for _, v := range []string{"", "gold4", "gold", "blue0"} {
		if _, e := ParsePillarCode(v); e == nil {
			t.Fatalf("expected fail: %q", v)
		}
	}
}

func TestPatchPreservesOtherModAndIsIdempotent(t *testing.T) {
	a, e := PatchCombinedTimelines(testBaseline(), "amazon", "gold2")
	if e != nil {
		t.Fatal(e)
	}
	b, e := PatchCombinedTimelines(a, "amazon", "gold2")
	if e != nil {
		t.Fatal(e)
	}
	var va, vb map[string]any
	ca, _ := stripJSONC(a)
	cb, _ := stripJSONC(b)
	if e = json.Unmarshal(ca, &va); e != nil {
		t.Fatal(e)
	}
	if e = json.Unmarshal(cb, &vb); e != nil {
		t.Fatal(e)
	}
	if va["otherMod"].(map[string]any)["keep"].(float64) != 123 {
		t.Fatal("other mod lost")
	}
	h := vb["timelines"].(map[string]any)["hth_dead"].(map[string]any)
	ev := h["animationEvents"].([]any)
	count := 0
	for _, x := range ev {
		m := x.(map[string]any)
		if n, ok := m["name"].(string); ok && len(n) >= len(PillarPrefix) && n[:len(PillarPrefix)] == PillarPrefix {
			count++
		}
	}
	if count != 7 {
		t.Fatalf("pillar event count=%d", count)
	}
}

func TestAtoBConvergesEvents(t *testing.T) {
	a, e := PatchCombinedTimelines(testBaseline(), "amazon", "red1")
	if e != nil {
		t.Fatal(e)
	}
	b, e := PatchCombinedTimelines(a, "amazon", "blue3")
	if e != nil {
		t.Fatal(e)
	}
	var v map[string]any
	clean, _ := stripJSONC(b)
	if e = json.Unmarshal(clean, &v); e != nil {
		t.Fatal(e)
	}
	ev := v["timelines"].(map[string]any)["hth_dead"].(map[string]any)["animationEvents"].([]any)
	for _, x := range ev {
		m := x.(map[string]any)
		if m["type"] == "SpawnVFX" {
			params := m["eventParams"].([]any)
			if params[0].(map[string]any)["value"] != "data/hd/vfx/colorlight/drop_light_blue3.particles" {
				t.Fatal("old variant remained in event")
			}
		}
	}
}

func TestBackupRestore(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "target", "data")
	dest := filepath.Join(data, "x.txt")
	os.MkdirAll(filepath.Dir(dest), 0755)
	os.WriteFile(dest, []byte("before"), 0644)
	plan := &InstallPlan{DataRoot: data, Writes: []WriteOp{{Rel: "x.txt", Dest: dest, Data: []byte("after")}, {Rel: "new.txt", Dest: filepath.Join(data, "new.txt"), Data: []byte("new")}}}
	bdir, _, e := CreateBackup(root, plan, "BeforeInstall")
	if e != nil {
		t.Fatal(e)
	}
	if e = ApplyPlan(plan); e != nil {
		t.Fatal(e)
	}
	ti := TargetInfo{DataRoot: data}
	if _, e = RestoreBackup(root, bdir, ti); e != nil {
		t.Fatal(e)
	}
	got, _ := os.ReadFile(dest)
	if string(got) != "before" {
		t.Fatal("restore mismatch")
	}
	if _, e = os.Stat(filepath.Join(data, "new.txt")); !os.IsNotExist(e) {
		t.Fatal("new file not removed")
	}
}

func TestPackagedSourceAndPlan(t *testing.T) {
	root, err := filepath.Abs("..")
	if err != nil {
		t.Fatal(err)
	}
	if _, statErr := os.Stat(filepath.Join(root, "data")); os.IsNotExist(statErr) {
		t.Skip("package data not present")
	}
	state, err := ValidateSource(root)
	if err != nil {
		t.Fatal(err)
	}
	if len(state.Files) < 90 {
		t.Fatalf("unexpected source file count: %d", len(state.Files))
	}

	target := filepath.Join(t.TempDir(), "M.mpq", "data")
	os.MkdirAll(filepath.Join(target, "hd", "character", "player", "amazon", "animation"), 0755)
	base, err := os.ReadFile(BaselinePath(root, "amazon"))
	if err != nil {
		t.Fatal(err)
	}
	clean, _ := stripJSONC(base)
	var doc map[string]any
	json.Unmarshal(clean, &doc)
	doc["OtherModFixture"] = map[string]any{"keep": "yes"}
	modded, _ := json.MarshalIndent(doc, "", "  ")
	os.WriteFile(filepath.Join(target, TimelineRelPath("amazon")), modded, 0644)

	plan, err := BuildInstallPlan(root, target, "magenta3")
	if err != nil {
		t.Fatal(err)
	}
	if len(plan.Writes) != 11 {
		t.Fatalf("writes=%d", len(plan.Writes))
	}
	if err := ApplyPlan(plan); err != nil {
		t.Fatal(err)
	}
	out, _ := os.ReadFile(filepath.Join(target, TimelineRelPath("amazon")))
	var got map[string]any
	json.Unmarshal(out, &got)
	if got["OtherModFixture"].(map[string]any)["keep"] != "yes" {
		t.Fatal("OtherMod fixture lost")
	}

	plan2, err := BuildInstallPlan(root, target, "red1")
	if err != nil {
		t.Fatal(err)
	}
	if err := ApplyPlan(plan2); err != nil {
		t.Fatal(err)
	}
	out, _ = os.ReadFile(filepath.Join(target, TimelineRelPath("amazon")))
	json.Unmarshal(out, &got)
	ev := got["timelines"].(map[string]any)["hth_dead"].(map[string]any)["animationEvents"].([]any)
	own := 0
	for _, item := range ev {
		m := item.(map[string]any)
		if n, _ := m["name"].(string); len(n) >= len(PillarPrefix) && n[:len(PillarPrefix)] == PillarPrefix {
			own++
		}
	}
	if own != 7 {
		t.Fatalf("own events=%d", own)
	}
}

func TestPatchPreservesJSONCOutsideContractAndInsertionOrder(t *testing.T) {
	raw := testBaseline()
	out, err := PatchCombinedTimelines(raw, "amazon", "gold2")
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Contains(out, []byte("// comment")) {
		t.Fatal("root comment was not preserved")
	}
	if !bytes.Contains(out, []byte(`"otherMod":{"keep":123,}`)) {
		t.Fatal("outside-contract JSONC formatting changed")
	}
	death := bytes.Index(out, []byte(`"hth_death"`))
	dead := bytes.Index(out, []byte(`"hth_dead"`))
	foo := bytes.Index(out, []byte(`"foo"`))
	if death < 0 || dead < 0 || foo < 0 || !(death < dead && dead < foo) {
		t.Fatalf("hth_dead was not inserted immediately after hth_death: death=%d dead=%d foo=%d", death, dead, foo)
	}
}

func TestAllPackagedVariantsPatchAllClasses(t *testing.T) {
	root, err := filepath.Abs("..")
	if err != nil {
		t.Fatal(err)
	}
	if _, statErr := os.Stat(filepath.Join(root, "data")); os.IsNotExist(statErr) {
		t.Skip("package data not present")
	}
	colors := []string{"red", "orange", "gold", "peach", "white", "yellow", "green", "cyan", "blue", "navy", "pink", "magenta"}
	for _, color := range colors {
		for _, pattern := range []string{"1", "2", "3"} {
			code := color + pattern
			for _, className := range PlayerClasses {
				raw, err := os.ReadFile(BaselinePath(root, className))
				if err != nil {
					t.Fatal(err)
				}
				out, err := PatchCombinedTimelines(raw, className, code)
				if err != nil {
					t.Fatalf("%s/%s: %v", className, code, err)
				}
				clean, err := stripJSONC(out)
				if err != nil {
					t.Fatal(err)
				}
				var v map[string]any
				if err := json.Unmarshal(clean, &v); err != nil {
					t.Fatal(err)
				}
				h := v["timelines"].(map[string]any)["hth_dead"].(map[string]any)
				ev := h["animationEvents"].([]any)
				count := 0
				for _, x := range ev {
					m := x.(map[string]any)
					n, _ := m["name"].(string)
					if strings.HasPrefix(n, PillarPrefix) {
						count++
						params := m["eventParams"].([]any)
						if got := params[0].(map[string]any)["value"]; got != "data/hd/vfx/colorlight/drop_light_"+code+".particles" {
							t.Fatalf("%s/%s wrong particle: %v", className, code, got)
						}
					}
				}
				if count != 7 {
					t.Fatalf("%s/%s event count=%d", className, code, count)
				}
			}
		}
	}
}
