package iace

import (
	"sort"
	"testing"
)

// techSpecDerivedTags lists every tag that deriveEnergyFromSpec (narrative_parser.go)
// can emit from a numeric spec. These are reachable by the pipeline even though
// no library entry declares them directly, so the closure must include them.
var techSpecDerivedTags = []string{
	"high_force", "crush_point", "high_voltage", "electrical_part",
	"high_temperature", "thermal_accumulation", "high_pressure",
	"rotating_part", "high_speed", "stored_energy",
}

// buildEmittableTagUniverse returns every tag the resolve/parse pipeline can
// ever produce: component tags, energy-source tags, keyword ExtraTags,
// tech-spec-derived tags, and all synonym expansions of those. A pattern that
// requires a tag outside this universe is a "ghost" — it can never fire for
// ANY machine, regardless of input. This is machine-type-independent: it
// measures the library's internal consistency, not a single project.
func buildEmittableTagUniverse() map[string]bool {
	universe := make(map[string]bool)
	add := func(t string) {
		if t == "" || universe[t] {
			return
		}
		universe[t] = true
		for _, syn := range tagSynonyms[t] {
			universe[syn] = true
		}
	}

	for _, c := range GetComponentLibrary() {
		for _, t := range c.Tags {
			add(t)
		}
	}
	for _, e := range GetEnergySources() {
		for _, t := range e.Tags {
			add(t)
		}
	}
	for _, k := range GetKeywordDictionary() {
		for _, t := range k.ExtraTags {
			add(t)
		}
	}
	for _, t := range techSpecDerivedTags {
		add(t)
	}
	return universe
}

// TestTagVocabulary_GhostPatterns is a generic, library-wide diagnostic. It
// finds every pattern whose RequiredComponentTags or RequiredEnergyTags
// reference a tag that the pipeline can never emit. Such patterns silently
// never fire — a systematic recall loss across ALL machine types, not just
// one ground-truth set.
//
// It is a reporting test (t.Log, no hard threshold) so it surfaces the full
// ghost list without breaking CI while we drive the count down. Run with:
//
//	go test -v -vet=off -run TestTagVocabulary_GhostPatterns ./internal/iace/
func TestTagVocabulary_GhostPatterns(t *testing.T) {
	universe := buildEmittableTagUniverse()
	t.Logf("Emittable tag universe: %d distinct tags", len(universe))

	patterns := collectAllPatterns()
	t.Logf("Total patterns in library: %d", len(patterns))

	// missingTag → list of pattern IDs that require it but can't get it
	ghostByTag := make(map[string][]string)
	ghostPatterns := make(map[string]bool)

	check := func(patternID, tag, kind string) {
		if universe[tag] {
			return
		}
		ghostByTag[tag] = append(ghostByTag[tag], patternID+"("+kind+")")
		ghostPatterns[patternID] = true
	}

	for _, p := range patterns {
		for _, t := range p.RequiredComponentTags {
			check(p.ID, t, "comp")
		}
		for _, t := range p.RequiredEnergyTags {
			check(p.ID, t, "energy")
		}
	}

	t.Logf("=== Ghost-Pattern Diagnostic ===")
	t.Logf("Patterns that can NEVER fire: %d / %d (%.1f%%)",
		len(ghostPatterns), len(patterns),
		100*float64(len(ghostPatterns))/float64(maxInt(len(patterns), 1)))
	t.Logf("Distinct unreachable required-tags: %d", len(ghostByTag))

	// Sort missing tags by how many patterns they kill (descending).
	type tagHit struct {
		tag   string
		count int
	}
	hits := make([]tagHit, 0, len(ghostByTag))
	for tag, ids := range ghostByTag {
		hits = append(hits, tagHit{tag, len(ids)})
	}
	sort.Slice(hits, func(i, j int) bool {
		if hits[i].count != hits[j].count {
			return hits[i].count > hits[j].count
		}
		return hits[i].tag < hits[j].tag
	})

	t.Logf("\n--- Unreachable tags (tag → #patterns killed) ---")
	for _, h := range hits {
		example := ghostByTag[h.tag]
		if len(example) > 6 {
			example = example[:6]
		}
		t.Logf("  %-28s %3d  e.g. %v", h.tag, h.count, example)
	}

	// Regression guard: every pattern's required tags MUST be emittable.
	// A new ghost means a pattern was added with a required tag that no
	// component/energy/keyword/synonym produces — it would silently never fire.
	if len(ghostPatterns) > 0 {
		t.Errorf("ghost patterns must be 0; found %d patterns requiring %d unreachable tags. "+
			"Add the tag to keyword_dictionary.go (emit side) or fix the pattern's required tag.",
			len(ghostPatterns), len(ghostByTag))
	}
}

// TestPatternSpecificity_PromiscuousPatterns is the precision counterpart to the
// ghost diagnostic. A "promiscuous" pattern has no MachineTypes gate AND no
// required component/energy tags — it fires for literally every machine that
// produces any tag at all. These are the dominant driver of false-positive
// "extra" hazards: a rich narrative makes hundreds of them fire. This measures
// the engine's structural precision ceiling, independent of any ground truth.
//
//	go test -v -vet=off -run TestPatternSpecificity_PromiscuousPatterns ./internal/iace/
func TestPatternSpecificity_PromiscuousPatterns(t *testing.T) {
	patterns := collectAllPatterns()

	var promiscuous, looselyGated int           // 0 / ≤1 discriminating signal
	gateHistogram := map[int]int{}              // #discriminating-signals → #patterns
	var promiscuousExamples []string

	for _, p := range patterns {
		// Count signals that actually discriminate BY MACHINE: machine-type
		// gate, required component tags, required energy tags, excluded tags.
		// Lifecycle/state/role gates rarely discriminate between machines.
		signals := 0
		if len(p.MachineTypes) > 0 {
			signals++
		}
		signals += len(p.RequiredComponentTags)
		signals += len(p.RequiredEnergyTags)
		signals += len(p.ExcludedComponentTags)

		gateHistogram[minInt(signals, 4)]++
		if signals == 0 {
			promiscuous++
			if len(promiscuousExamples) < 12 {
				promiscuousExamples = append(promiscuousExamples, p.ID)
			}
		}
		if signals <= 1 {
			looselyGated++
		}
	}

	t.Logf("=== Pattern Specificity Diagnostic ===")
	t.Logf("Total patterns: %d", len(patterns))
	t.Logf("Promiscuous (0 machine-discriminating signals): %d (%.1f%%)",
		promiscuous, 100*float64(promiscuous)/float64(maxInt(len(patterns), 1)))
	t.Logf("Loosely gated (≤1 signal): %d (%.1f%%)",
		looselyGated, 100*float64(looselyGated)/float64(maxInt(len(patterns), 1)))
	t.Logf("\n--- Discriminating-signal histogram (signals → #patterns) ---")
	for s := 0; s <= 4; s++ {
		label := ""
		if s == 4 {
			label = "+"
		}
		t.Logf("  %d%s signals: %d patterns", s, label, gateHistogram[s])
	}
	t.Logf("\n  Promiscuous examples: %v", promiscuousExamples)
}

func minInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}
