package linthost

import (
  "flag"
  "fmt"
  shimast "github.com/microsoft/typescript-go/shim/ast"
  "path/filepath"
  "runtime"
  "sort"
  "strings"
  "sync"
  "testing"
)

// behavioralWitnessKind describes the production prerequisite exercised by a
// positive rule test. The kind is reporting metadata, not an exemption: every
// witness still has to reach a real rule through Engine or the check command
// and observe that rule's own diagnostic before it is recorded.
type behavioralWitnessKind string

const (
  behavioralWitnessEngine   behavioralWitnessKind = "engine"
  behavioralWitnessOptions  behavioralWitnessKind = "options"
  behavioralWitnessFilename behavioralWitnessKind = "filename"
  behavioralWitnessProject  behavioralWitnessKind = "project"
  behavioralWitnessChecker  behavioralWitnessKind = "checker"
  behavioralWitnessPlatform behavioralWitnessKind = "platform"
)

type behavioralWitness struct {
  Rule    string
  Route   string
  Kind    behavioralWitnessKind
  Sources []string
}

var behavioralWitnessRegistry = struct {
  sync.Mutex
  candidates map[string]map[string]behavioralWitness
}{candidates: map[string]map[string]behavioralWitness{}}

// recordBehavioralWitness is called only after a positive assertion has
// verified a production finding. Multiple positive regression tests may exist
// for one rule; the audit publishes their lexicographically first test name as
// the single canonical coverage route so the contract remains deterministic.
func recordBehavioralWitness(t *testing.T, ruleName string, kind behavioralWitnessKind) {
  t.Helper()
  recordBehavioralWitnessRoute(behavioralWitness{
    Rule:    ruleName,
    Route:   t.Name(),
    Kind:    kind,
    Sources: behavioralWitnessSourceFiles(),
  })
}

func behavioralWitnessSourceFiles() []string {
  callers := make([]uintptr, 32)
  count := runtime.Callers(2, callers)
  frames := runtime.CallersFrames(callers[:count])
  source := ""
  for {
    frame, more := frames.Next()
    if strings.HasSuffix(frame.File, "_test.go") {
      // Keep the outermost test frame, not shared recorder/helper frames, so a
      // witness names the test that owns the positive case.
      source = filepath.Base(frame.File)
    }
    if !more {
      break
    }
  }
  if source == "" {
    return nil
  }
  return []string{source}
}

func recordBehavioralWitnessRoute(candidate behavioralWitness) {
  behavioralWitnessRegistry.Lock()
  defer behavioralWitnessRegistry.Unlock()
  routes := behavioralWitnessRegistry.candidates[candidate.Rule]
  if routes == nil {
    routes = map[string]behavioralWitness{}
    behavioralWitnessRegistry.candidates[candidate.Rule] = routes
  }
  routes[candidate.Route] = candidate
}

func recordFindingBehavioralWitnesses(
  t *testing.T,
  findings []*Finding,
  kind behavioralWitnessKind,
) {
  t.Helper()
  recordFindingBehavioralWitnessesByRule(t, findings, func(string) behavioralWitnessKind {
    return kind
  })
}

func recordFindingBehavioralWitnessesByRule(
  t *testing.T,
  findings []*Finding,
  kindForRule func(string) behavioralWitnessKind,
) {
  t.Helper()
  recorded := map[string]struct{}{}
  for _, finding := range findings {
    if finding == nil || finding.Rule == "" {
      continue
    }
    if _, ok := recorded[finding.Rule]; ok {
      continue
    }
    recorded[finding.Rule] = struct{}{}
    recordBehavioralWitness(t, finding.Rule, kindForRule(finding.Rule))
  }
}

func recordedBehavioralWitnesses() map[string][]behavioralWitness {
  behavioralWitnessRegistry.Lock()
  defer behavioralWitnessRegistry.Unlock()
  out := make(map[string][]behavioralWitness, len(behavioralWitnessRegistry.candidates))
  for ruleName, routes := range behavioralWitnessRegistry.candidates {
    for _, witness := range routes {
      witness.Sources = append([]string(nil), witness.Sources...)
      out[ruleName] = append(out[ruleName], witness)
    }
  }
  return out
}

// registeredBuiltInNonFormatRuleSet returns the canonical built-in, non-format
// rule set. Runtime contributors and test-only direct registrations have no
// built-in TypeScript family property, while format rules are configured
// through ITtscLintFormat instead of ITtscLintRules.
func registeredBuiltInNonFormatRuleSet() map[string]struct{} {
  out := make(map[string]struct{}, len(AllRuleNames()))
  for _, name := range AllRuleNames() {
    if !isRegisteredBuiltInNonFormatRule(name, LookupRule(name)) {
      continue
    }
    out[name] = struct{}{}
  }
  return out
}

// isRegisteredBuiltInNonFormatRule classifies registry entries by runtime
// provenance rather than namespace spelling. The append-only built-in rule-code
// ledger excludes arbitrary direct registrations, and the structural adapter
// check also excludes a contributor that reuses a retired ledger name.
func isRegisteredBuiltInNonFormatRule(name string, candidate Rule) bool {
  return isRegisteredBuiltInRule(name, candidate) && !isFormatRule(candidate)
}

// isRegisteredBuiltInFormatRule is the format-side twin used to compare the
// live format-block expansion with every native formatter registration.
func isRegisteredBuiltInFormatRule(name string, candidate Rule) bool {
  return isRegisteredBuiltInRule(name, candidate) && isFormatRule(candidate)
}

func isRegisteredBuiltInRule(name string, candidate Rule) bool {
  if candidate == nil {
    return false
  }
  switch candidate.(type) {
  case contributorAdapter, formatContributorAdapter:
    return false
  }
  _, builtIn := builtInRuleCodes[name]
  return builtIn
}

// behavioralWitnessPublicRuleSet is the rule set the witness audit requires a
// positive production witness for: every user-facing registered rule. It is the
// built-in non-format set (registeredBuiltInNonFormatRuleSet) plus the format/*
// family. Format rules are user-facing but configured through the `format`
// block rather than a typed `rules` key, so registeredBuiltInNonFormatRuleSet
// excludes them. The witness audit asks a different question ("does every
// user-facing rule fire in production?"), so it must not: the formatter family
// is precisely the over-match-prone surface the witness doctrine exists to
// guard. Format rules earn their witnesses through the dedicated
// format tests in this package (the `format_*_test.go` files), the same route
// other rules that cannot run the flat corpus already use.
func behavioralWitnessPublicRuleSet() map[string]struct{} {
  public := registeredBuiltInNonFormatRuleSet()
  for _, name := range AllRuleNames() {
    if strings.HasPrefix(name, "format/") {
      public[name] = struct{}{}
    }
  }
  return public
}

// verifyRecordedBehavioralWitnessCoverage runs after the package test suite.
// A rule can enter the canonical map only after a positive assertion executed,
// so registry parity can no longer be satisfied by an inert rule object.
func verifyRecordedBehavioralWitnessCoverage() error {
  candidates := recordedBehavioralWitnesses()
  public := behavioralWitnessPublicRuleSet()
  _, err := auditBehavioralWitnesses(public, candidates)
  if err != nil {
    return err
  }
  return verifyRequiredBehavioralWitnessKinds(public, candidates)
}

func verifyRequiredBehavioralWitnessKinds(
  public map[string]struct{},
  candidates map[string][]behavioralWitness,
) error {
  seen := map[behavioralWitnessKind]struct{}{}
  for ruleName, routes := range candidates {
    if _, ok := public[ruleName]; !ok {
      continue
    }
    for _, candidate := range routes {
      seen[candidate.Kind] = struct{}{}
    }
  }
  required := []behavioralWitnessKind{
    behavioralWitnessEngine,
    behavioralWitnessOptions,
    behavioralWitnessFilename,
    behavioralWitnessProject,
    behavioralWitnessChecker,
    behavioralWitnessPlatform,
  }
  missing := make([]string, 0)
  for _, kind := range required {
    if _, ok := seen[kind]; !ok {
      missing = append(missing, string(kind))
    }
  }
  if len(missing) != 0 {
    return fmt.Errorf("behavioral witness audit did not exercise prerequisite kinds: %v", missing)
  }
  return nil
}

// shouldVerifyRecordedBehavioralWitnessCoverage preserves focused test and
// test-listing workflows. The aggregate contract is evaluated only when the
// complete package suite ran; CI and `go test` use that path.
func shouldVerifyRecordedBehavioralWitnessCoverage() bool {
  for _, name := range []string{"test.run", "test.skip", "test.list", "test.fuzz"} {
    value := flag.Lookup(name)
    if value != nil && value.Value.String() != "" {
      return false
    }
  }
  short := flag.Lookup("test.short")
  if short != nil && short.Value.String() == "true" {
    return false
  }
  return true
}

// auditBehavioralWitnesses validates every candidate for the supplied public
// set, then returns one lexically deterministic route per identity. The caller
// owns public-set selection, including formatter rules. Unselected test/ and
// demo/ witnesses are tolerated; other stale identities are rejected.
func auditBehavioralWitnesses(
  public map[string]struct{},
  candidates map[string][]behavioralWitness,
) (map[string]behavioralWitness, error) {
  canonical := make(map[string]behavioralWitness, len(public))
  missing := make([]string, 0)
  stale := make([]string, 0)
  invalid := make([]string, 0)

  for ruleName := range public {
    routes := append([]behavioralWitness(nil), candidates[ruleName]...)
    if len(routes) == 0 {
      missing = append(missing, ruleName)
      continue
    }
    sort.Slice(routes, func(i, j int) bool {
      if routes[i].Route != routes[j].Route {
        return routes[i].Route < routes[j].Route
      }
      return routes[i].Kind < routes[j].Kind
    })
    valid := true
    for _, candidate := range routes {
      if candidate.Rule != ruleName || candidate.Route == "" ||
        !validBehavioralWitnessKind(candidate.Kind) ||
        !validBehavioralWitnessSources(candidate.Sources) {
        invalid = append(invalid, fmt.Sprintf("%s=%+v", ruleName, candidate))
        valid = false
      }
    }
    if valid {
      canonical[ruleName] = routes[0]
    }
  }
  for ruleName := range candidates {
    if _, ok := public[ruleName]; !ok && !isNonPublicRuleName(ruleName) {
      stale = append(stale, ruleName)
    }
  }
  sort.Strings(missing)
  sort.Strings(stale)
  sort.Strings(invalid)
  if len(missing) != 0 || len(stale) != 0 || len(invalid) != 0 {
    parts := make([]string, 0, 3)
    if len(missing) != 0 {
      parts = append(parts, fmt.Sprintf("public rules without a positive production witness: %v", missing))
    }
    if len(stale) != 0 {
      parts = append(parts, fmt.Sprintf("witnesses for non-public rules: %v", stale))
    }
    if len(invalid) != 0 {
      parts = append(parts, fmt.Sprintf("invalid witness records: %v", invalid))
    }
    return nil, fmt.Errorf("behavioral witness audit failed: %s", strings.Join(parts, "; "))
  }
  return canonical, nil
}

func validBehavioralWitnessKind(kind behavioralWitnessKind) bool {
  switch kind {
  case behavioralWitnessEngine,
    behavioralWitnessOptions,
    behavioralWitnessFilename,
    behavioralWitnessProject,
    behavioralWitnessChecker,
    behavioralWitnessPlatform:
    return true
  default:
    return false
  }
}

func behavioralWitnessKindForRule(ruleName string) behavioralWitnessKind {
  if rule := LookupRule(ruleName); ruleNeedsTypeChecker(rule) {
    return behavioralWitnessChecker
  }
  return behavioralWitnessEngine
}

func behavioralWitnessKindForOptions(
  ruleName string,
  options RuleOptionsMap,
) behavioralWitnessKind {
  if _, ok := options[ruleName]; ok {
    return behavioralWitnessOptions
  }
  return behavioralWitnessEngine
}

func validBehavioralWitnessSources(sources []string) bool {
  if len(sources) != 1 {
    return false
  }
  source := sources[0]
  return source != "" && filepath.Base(source) == source &&
    strings.HasSuffix(source, "_test.go")
}

// isNonPublicRuleName reports whether a recorded witness belongs to a rule that
// is intentionally outside the public audit set. Only the test/ and demo/
// families qualify: format/* rules ARE public (see behavioralWitnessPublicRuleSet),
// so a stray format witness for an unregistered id is now correctly flagged
// instead of silently tolerated.
func isNonPublicRuleName(ruleName string) bool {
  return strings.HasPrefix(ruleName, "test/") ||
    strings.HasPrefix(ruleName, "demo/")
}

type inertBehavioralWitnessRule struct{}

func (inertBehavioralWitnessRule) Name() string {
  return "test/behavioral-witness-inert"
}

func (inertBehavioralWitnessRule) Visits() []shimast.Kind {
  return []shimast.Kind{shimast.KindSourceFile}
}

func (inertBehavioralWitnessRule) Check(*Context, *shimast.Node) {}
