package world

import (
	"fmt"
	"os"
	"sort"
	"testing"

	"regionio/internal/worldgen"
)

// base_terrain_diagnostic_test.go splits the vanilla fixture's block
// mismatches into classes by what could have written each side. The ore-replay
// diagnostics measure how feature placement drifts; this one attributes it.
//
// Every fixture cell is compared against the undecorated base chunk. Plain-
// versus-plain pairs (air family, fluids, bedrock, stone, deepslate) used to
// be read as direct base-terrain defects, but TestVanillaBaseTerrainParity
// settled that question: against a vanilla capture whose biomes carry no
// features, our undecorated pipeline matches exactly. Plain pairs here are
// therefore feature outputs wearing plain states — monster-room cave_air,
// geode voids, lava-lake lava — and the sample coordinates point at the
// feature, not at the noise stack. Cells where exactly one side is a
// recognizably feature-placed state count as a flip toward that side; the
// remaining ambiguous pairs (disk outputs such as clay look like natural
// terrain) are bucketed separately.
//
// Run with REGIONIO_BASE_TERRAIN_DIAGNOSTIC=1.

var baseTerrainPlainNames = map[string]bool{
	"minecraft:air":       true,
	"minecraft:cave_air":  true,
	"minecraft:water":     true,
	"minecraft:lava":      true,
	"minecraft:bedrock":   true,
	"minecraft:stone":     true,
	"minecraft:deepslate": true,
}

// baseTerrainFamily names the feature that would have placed a state, or ""
// when the state is not attributable to a single replayed feature family.
func baseTerrainFamily(id uint16) string {
	if id == 0 {
		return ""
	}
	if isOreState(id) {
		return "ore"
	}
	switch stateLabel(id) {
	case "minecraft:calcite", "minecraft:smooth_basalt",
		"minecraft:amethyst_block", "minecraft:budding_amethyst":
		return "geode"
	case "minecraft:magma_block":
		return "magma"
	case "minecraft:moss_block", "minecraft:moss_carpet",
		"minecraft:azalea", "minecraft:flowering_azalea",
		"minecraft:hanging_roots", "minecraft:rooted_dirt",
		"minecraft:big_dripleaf", "minecraft:small_dripleaf":
		return "lush-patch"
	}
	return ""
}

type basePairKey struct{ got, want uint16 }

func TestBaseTerrainMismatchDiagnostic(t *testing.T) {
	if os.Getenv("REGIONIO_BASE_TERRAIN_DIAGNOSTIC") != "1" {
		t.Skip("set REGIONIO_BASE_TERRAIN_DIAGNOSTIC=1 to classify fixture mismatches")
	}
	fixtures, seed := loadOreFixtureChunks(t)

	od, err := worldgen.LoadOverworldFinalDensity(seed)
	if err != nil {
		t.Fatal(err)
	}
	fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel)
	veins := worldgen.NewOreVeinifier(od)
	carver, err := worldgen.NewCarver(od, seed)
	if err != nil {
		t.Fatal(err)
	}
	initCarverReplaceable(carver.ReplaceableBlocks())

	var total, exact int
	baseVsBase := make(map[basePairKey]int)
	pairSamples := make(map[basePairKey][]string)
	pairYRange := make(map[basePairKey][2]int)
	bandCounts := make(map[string]int)
	var fluidFlip int
	missingByFamily := make(map[string]int)
	extraByFamily := make(map[string]int)
	ambiguous := make(map[basePairKey]int)

	for _, fixture := range fixtures {
		chunk := generateVanillaWithoutDecoration(od, fluidPicker, veins, carver, seed, fixture.x, fixture.z)
		index := 0
		for y := MinY; y < MinY+WorldHeight; y++ {
			for z := 0; z < 16; z++ {
				for x := 0; x < 16; x++ {
					got := chunk.GetBlock(x, y, z)
					want := fixture.blocks[index]
					index++
					total++
					if got == want {
						exact++
						continue
					}
					gotName := stateLabel(got)
					wantName := stateLabel(want)
					gotFamily := baseTerrainFamily(got)
					wantFamily := baseTerrainFamily(want)

					switch {
					case baseTerrainPlainNames[gotName] && baseTerrainPlainNames[wantName]:
						key := basePairKey{got, want}
						baseVsBase[key]++
						rng := pairYRange[key]
						if rng[0] == 0 || y < rng[0] {
							rng[0] = y
						}
						if y > rng[1] {
							rng[1] = y
						}
						pairYRange[key] = rng
						if len(pairSamples[key]) < 8 {
							absX, absZ := int(fixture.x)*16+x, int(fixture.z)*16+z
							pairSamples[key] = append(pairSamples[key], fmt.Sprintf("(%d,%d,%d)", absX, y, absZ))
						}
						band := "underground"
						switch {
						case y < 0:
							band = "deep"
						case y >= SeaLevel+16:
							band = "surface"
						case y >= SeaLevel:
							band = "waterline"
						}
						bandCounts[band]++
						if (isWaterState(got) || isLavaState(got)) != (isWaterState(want) || isLavaState(want)) {
							fluidFlip++
						}
					case gotFamily != "" && wantFamily != "":
						ambiguous[basePairKey{got, want}]++
					case wantFamily != "":
						missingByFamily[wantFamily]++
					case gotFamily != "":
						extraByFamily[gotFamily]++
					default:
						ambiguous[basePairKey{got, want}]++
					}
				}
			}
		}
	}

	t.Logf("undecorated base vs vanilla final: exact %d/%d (%.3f%%)", exact, total, percent(exact, total))

	baseTotal := 0
	keys := make([]basePairKey, 0, len(baseVsBase))
	for key, count := range baseVsBase {
		keys = append(keys, key)
		baseTotal += count
	}
	sort.Slice(keys, func(i, j int) bool {
		if baseVsBase[keys[i]] != baseVsBase[keys[j]] {
			return baseVsBase[keys[i]] > baseVsBase[keys[j]]
		}
		return stateLabel(keys[i].want)+stateLabel(keys[i].got) < stateLabel(keys[j].want)+stateLabel(keys[j].got)
	})
	t.Logf("base-vs-base mismatches %d (%.3f%% of fixture), fluidness flips %d, bands deep=%d underground=%d waterline=%d surface=%d",
		baseTotal, percent(baseTotal, total), fluidFlip,
		bandCounts["deep"], bandCounts["underground"], bandCounts["waterline"], bandCounts["surface"])
	const maxBasePairs = 20
	for i, key := range keys {
		if i >= maxBasePairs {
			t.Logf("... %d more base-vs-base pairs", len(keys)-maxBasePairs)
			break
		}
		rng := pairYRange[key]
		t.Logf("base %s -> %s: %d, y=%d..%d, samples %v",
			stateLabel(key.got), stateLabel(key.want), baseVsBase[key], rng[0], rng[1], pairSamples[key])
	}

	logFamilyTotals(t, "missing", missingByFamily)
	logFamilyTotals(t, "extra", extraByFamily)

	ambTotal := 0
	ambKeys := make([]basePairKey, 0, len(ambiguous))
	for key, count := range ambiguous {
		ambKeys = append(ambKeys, key)
		ambTotal += count
	}
	sort.Slice(ambKeys, func(i, j int) bool {
		return ambiguous[ambKeys[i]] > ambiguous[ambKeys[j]]
	})
	t.Logf("ambiguous pairs %d (feature-shaped outputs such as disks over natural-looking terrain)", ambTotal)
	for i, key := range ambKeys {
		if i >= 10 {
			break
		}
		t.Logf("ambiguous %s -> %s: %d", stateLabel(key.got), stateLabel(key.want), ambiguous[key])
	}
}

func logFamilyTotals(t *testing.T, label string, totals map[string]int) {
	t.Helper()
	names := make([]string, 0, len(totals))
	sum := 0
	for name, count := range totals {
		names = append(names, name)
		sum += count
	}
	sort.Slice(names, func(i, j int) bool { return totals[names[i]] > totals[names[j]] })
	formatted := ""
	for i, name := range names {
		if i > 0 {
			formatted += ", "
		}
		formatted += fmt.Sprintf("%s=%d", name, totals[name])
	}
	t.Logf("%s feature cells %d (%s)", label, sum, formatted)
}
