package score

import (
	"bytes"
	"encoding/json"
	"fmt"
	"maps"
	"math/big"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/devrites/devrites/internal/overhaul/ovio"
)

var doms = []string{"correctness", "security", "reliability", "performance", "tests", "architecture", "ux", "operations"}

var fp = strings.Repeat("a", 64)

func ctl(i int, d string, w any, lanes ...string) map[string]any {
	if len(lanes) == 0 {
		lanes = []string{"backend"}
	}
	return map[string]any{"id": fmt.Sprintf("C-%03d", i), "domain": d, "lanes": lanes, "weight": w, "hard_gate": false, "evaluation": "executed"}
}

func rubricOf(controls []map[string]any) map[string]any {
	return map[string]any{"schema": "overhaul.rubric/1", "rev": 1, "lanes": []string{"frontend", "backend"},
		"controls": controls, "domains": map[string]any{"correctness": 20, "security": 20, "reliability": 15,
			"performance": 15, "tests": 10, "architecture": 10, "ux": 5, "operations": 5}}
}

func resOf(status map[string]string) map[string]any {
	r := map[string]any{}
	for k, v := range status {
		r[k] = map[string]any{"status": v, "evidence": []string{"EV-1"}}
	}
	return map[string]any{"schema": "overhaul.results/1", "subject": "candidate", "fingerprint": fp, "results": r}
}

func passing() map[string]any {
	g := map[string]any{}
	for _, id := range auditGates {
		g[id] = map[string]any{"status": "PASS", "evidence": []string{"EV-g"}}
	}
	return map[string]any{"mode": "audit", "gates": g}
}

func allPass(cs []map[string]any) map[string]string {
	st := map[string]string{}
	for _, c := range cs {
		st[c["id"].(string)] = "PASS"
	}
	return st
}

// norm round-trips v through JSON with UseNumber, exactly as file input arrives.
func norm(t *testing.T, v any) map[string]any {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatal(err)
	}
	dec := json.NewDecoder(bytes.NewReader(b))
	dec.UseNumber()
	var m map[string]any
	if err := dec.Decode(&m); err != nil {
		t.Fatal(err)
	}
	return m
}

func tryScore(t *testing.T, r, s, g any) (map[string]any, error) {
	t.Helper()
	out, err := score(norm(t, r), norm(t, s), norm(t, g))
	if err != nil {
		return nil, err
	}
	return norm(t, out), nil
}

func mustScore(t *testing.T, r, s, g any) map[string]any {
	t.Helper()
	out, err := tryScore(t, r, s, g)
	if err != nil {
		t.Fatalf("score: %v", err)
	}
	return out
}

func get(v any, path string) string { return fmt.Sprint(ovio.Get(v, path)) }

func exact(t *testing.T, v any, path string) *big.Rat {
	t.Helper()
	r, ok := new(big.Rat).SetString(get(v, path))
	if !ok {
		t.Fatalf("%s is not exact: %v", path, ovio.Get(v, path))
	}
	return r
}

func base() []map[string]any {
	var b []map[string]any
	for i, d := range doms {
		b = append(b, ctl(i, d, 5, "frontend", "backend"))
	}
	return b
}

func workedExample() ([]map[string]any, map[string]string) {
	ex := append(base(), ctl(10, "correctness", 5), ctl(11, "correctness", 5), ctl(12, "correctness", 3), ctl(13, "correctness", 1))
	st := allPass(ex)
	st["C-013"] = "FAIL"
	return ex, st
}

func laneMasking() []map[string]any {
	l := base()
	for k := range 10 {
		l = append(l, ctl(30+k, "correctness", 5, "backend"))
	}
	return append(l, ctl(40, "correctness", 5, "frontend"))
}

// 1. Worked example: correctness 5,5,5,3 PASS + 1 FAIL -> D=180/19, Q=18800/1900.
func TestWorkedExample(t *testing.T) {
	ex, st := workedExample()
	out := mustScore(t, rubricOf(ex), resOf(st), passing())
	if got := get(out, "global.domains.correctness.exact"); got != "180/19" {
		t.Fatalf("correctness = %s", got)
	}
	if got := get(out, "global.Q.exact"); got != "188/19" {
		t.Fatalf("Q = %s", got)
	}
	if got := get(out, "global.Q.decimal"); got != "9.894736842105" {
		t.Fatalf("decimal = %s", got)
	}
	if v := get(out, "readiness_verdict"); v != "MEETS_TARGETS" {
		t.Fatalf("verdict = %s", v)
	}
}

// 2. Same numbers, failing control is a hard gate -> NOT_READY despite Q 9.89.
func TestHardGateBlocks(t *testing.T) {
	ex, st := workedExample()
	ex[len(ex)-1]["hard_gate"] = true
	out := mustScore(t, rubricOf(ex), resOf(st), passing())
	if get(out, "readiness_verdict") != "NOT_READY" || get(out, "gates.G-MANDATORY") != "FAIL" {
		t.Fatalf("verdict %s, G-MANDATORY %s", get(out, "readiness_verdict"), get(out, "gates.G-MANDATORY"))
	}
	if got := get(out, "global.Q.exact"); got != "188/19" {
		t.Fatalf("Q = %s", got)
	}
}

// 3. Q exactly 9.696 fails 9.7 at full precision.
func TestFullPrecisionThreshold(t *testing.T) {
	var cs []map[string]any
	st := map[string]string{}
	i := 100
	for _, d := range doms {
		n := 1
		if d == "correctness" || d == "security" {
			n = 250
		}
		for k := range n {
			cs = append(cs, ctl(i, d, 1, "frontend", "backend"))
			st[fmt.Sprintf("C-%03d", i)] = "PASS"
			if n == 250 && k < 19 {
				st[fmt.Sprintf("C-%03d", i)] = "FAIL"
			}
			i++
		}
	}
	out := mustScore(t, rubricOf(cs), resOf(st), passing())
	if got := get(out, "global.Q.exact"); got != "1212/125" {
		t.Fatalf("Q = %s", got)
	}
	row := ovio.List(out["thresholds"])[0]
	if get(row, "scope") != "global" || get(row, "result") != "FAIL" || get(out, "readiness_verdict") != "NOT_READY" {
		t.Fatalf("row %v verdict %s", row, get(out, "readiness_verdict"))
	}
}

// 4. Zero denominator: an applicable domain whose only control is N/A -> NOT_ASSESSABLE, never 10.
func TestZeroDenominatorNotAssessable(t *testing.T) {
	b := base()
	z := make([]map[string]any, len(b))
	for i, c := range b {
		z[i] = maps.Clone(c)
	}
	z[6]["na"] = map[string]any{"reason": "no UI", "evidence": []string{"EV-2"}, "approved": true}
	out := mustScore(t, rubricOf(z), resOf(allPass(b)), passing())
	if ovio.Get(out, "global.Q") != nil || get(out, "readiness_verdict") != "NOT_ASSESSABLE" {
		t.Fatalf("Q %v verdict %s", ovio.Get(out, "global.Q"), get(out, "readiness_verdict"))
	}
}

// 5. Global 59/6 and backend 10 cannot mask frontend 9.0; shared controls count once globally.
func TestLaneMasking(t *testing.T) {
	l := laneMasking()
	st := allPass(l)
	st["C-040"] = "FAIL"
	out := mustScore(t, rubricOf(l), resOf(st), passing())
	if row := ovio.List(out["thresholds"])[0]; get(row, "result") != "PASS" || get(out, "global.Q.exact") != "59/6" {
		t.Fatalf("global row %v Q %s", row, get(out, "global.Q.exact"))
	}
	if get(out, "lanes.frontend.Q.exact") != "9/1" || get(out, "lanes.backend.Q.exact") != "10/1" {
		t.Fatalf("lanes %v", out["lanes"])
	}
	laneRow := "missing"
	for _, row := range ovio.List(out["thresholds"]) {
		if get(row, "scope") == "lane frontend" {
			laneRow = get(row, "result")
		}
	}
	if laneRow != "FAIL" {
		t.Fatalf("lane frontend threshold row = %s", laneRow)
	}
	if get(out, "readiness_verdict") != "NOT_READY" {
		t.Fatalf("verdict %s", get(out, "readiness_verdict"))
	}
	if get(out, "counts.controls") != "19" || get(out, "counts.lane_appearances") != "27" {
		t.Fatalf("counts %v", out["counts"])
	}
}

// 6. PASS without evidence counts as UNKNOWN; UNKNOWN earns zero, not N/A.
func TestPassWithoutEvidenceIsUnknown(t *testing.T) {
	b := base()
	r := resOf(allPass(b))
	r["results"].(map[string]any)["C-000"].(map[string]any)["evidence"] = []string{}
	out := mustScore(t, rubricOf(b), r, passing())
	if get(out, "status_by_control.C-000") != "UNKNOWN" || get(out, "global.domains.correctness.exact") != "0/1" {
		t.Fatalf("status %s correctness %s", get(out, "status_by_control.C-000"), get(out, "global.domains.correctness.exact"))
	}
	if len(ovio.List(out["downgrades"])) != 1 {
		t.Fatalf("downgrades %v", out["downgrades"])
	}
}

// 7. Fixed-catalog monotonicity: turning any PASS into FAIL never raises any score.
func TestMonotonicity(t *testing.T) {
	l := laneMasking()
	full := allPass(l)
	top := mustScore(t, rubricOf(l), resOf(full), passing())
	for id := range full {
		worse := maps.Clone(full)
		worse[id] = "FAIL"
		low := mustScore(t, rubricOf(l), resOf(worse), passing())
		for _, p := range []string{"global.Q.exact", "lanes.frontend.Q.exact", "lanes.backend.Q.exact"} {
			if exact(t, low, p).Cmp(exact(t, top, p)) > 0 {
				t.Fatalf("failing %s raised %s", id, p)
			}
		}
		if exact(t, low, "global.Q.exact").Cmp(exact(t, top, "global.Q.exact")) >= 0 {
			t.Fatalf("failing %s did not lower global Q", id)
		}
	}
}

// Adding a FAIL or UNKNOWN control strictly lowers its own domain and never raises any domain.
func TestAddedNonPassControlLowersItsDomain(t *testing.T) {
	b := base()
	top := mustScore(t, rubricOf(b), resOf(allPass(b)), passing())
	for _, d := range doms {
		for _, status := range []string{"FAIL", "UNKNOWN"} {
			t.Run(d+"/"+status, func(t *testing.T) {
				cs := append(base(), ctl(50, d, 1, "frontend", "backend"))
				st := allPass(cs)
				st["C-050"] = status
				out := mustScore(t, rubricOf(cs), resOf(st), passing())
				for _, o := range doms {
					p := "global.domains." + o + ".exact"
					cmp := exact(t, out, p).Cmp(exact(t, top, p))
					if o == d && cmp >= 0 {
						t.Fatalf("%s control did not lower %s: %s", status, d, get(out, p))
					}
					if cmp > 0 {
						t.Fatalf("%s control in %s raised %s", status, d, o)
					}
				}
			})
		}
	}
}

// A control shared by two lanes adds its weight to the global domain denominator once.
func TestSharedControlWeightCountsOnceGlobally(t *testing.T) {
	cs := []map[string]any{ctl(0, "correctness", 5, "frontend", "backend"), ctl(1, "correctness", 5, "backend")}
	for _, d := range doms[1:] {
		cs = append(cs, ctl(2+len(cs), d, 5, "frontend", "backend"))
	}
	st := allPass(cs)
	st["C-001"] = "FAIL"
	out := mustScore(t, rubricOf(cs), resOf(st), passing())
	if got := get(out, "global.domains.correctness.exact"); got != "5/1" {
		t.Fatalf("correctness = %s, want 5/1", got)
	}
}

// 8. Duplicate control ids, weights off the 1/3/5 scale and unknown evaluations are invalid input.
func TestInvalidCatalog(t *testing.T) {
	zero := base()
	zero[0] = maps.Clone(zero[0])
	zero[0]["weight"] = 0
	two := base()
	two[0] = maps.Clone(two[0])
	two[0]["weight"] = 2
	noEval := base()
	noEval[0] = maps.Clone(noEval[0])
	delete(noEval[0], "evaluation")
	for _, tc := range []struct {
		cs   []map[string]any
		want string
	}{{append(base(), base()[0]), "duplicate control id"}, {zero, "must be > 0"}, {two, "weight must be 1, 3 or 5"},
		{noEval, "evaluation must be"}} {
		if _, err := tryScore(t, rubricOf(tc.cs), resOf(nil), passing()); err == nil || !strings.Contains(err.Error(), tc.want) {
			t.Fatalf("want %q, got %v", tc.want, err)
		}
	}
}

// 9. Approved sequential degradation is the only open gate -> qualified outcome.
func TestApprovedDegradation(t *testing.T) {
	b := base()
	g := passing()
	g["gates"].(map[string]any)["G-CONCURRENCY"] = map[string]any{"status": "WAIVED_OPERATIONAL", "approval": "AP-2"}
	out := mustScore(t, rubricOf(b), resOf(allPass(b)), g)
	if v := get(out, "readiness_verdict"); v != "MEETS_TARGETS_WITH_APPROVED_DEGRADATION" {
		t.Fatalf("verdict %s", v)
	}
}

// 10. Missing gate evidence -> UNKNOWN -> NOT_READY.
func TestGatePassWithoutEvidence(t *testing.T) {
	b := base()
	g := passing()
	g["gates"].(map[string]any)["G-COVERAGE"] = map[string]any{"status": "PASS"}
	out := mustScore(t, rubricOf(b), resOf(allPass(b)), g)
	if get(out, "gates.G-COVERAGE") != "UNKNOWN" || get(out, "readiness_verdict") != "NOT_READY" {
		t.Fatalf("gate %s verdict %s", get(out, "gates.G-COVERAGE"), get(out, "readiness_verdict"))
	}
}

// 11. A rubric cannot lower the policy thresholds or drop a domain without a recorded exclusion.
func TestWeakenedRubric(t *testing.T) {
	b := base()
	low := rubricOf(b)
	low["policy"] = map[string]any{"global_min": "0"}
	var four []map[string]any
	for i, d := range []string{"tests", "architecture", "ux", "operations"} {
		four = append(four, ctl(i+1, d, 5))
	}
	partial := rubricOf(four)
	partial["domains"] = map[string]any{"tests": 10, "architecture": 10, "ux": 5, "operations": 5}
	for _, tc := range []struct {
		r    map[string]any
		st   map[string]string
		want string
	}{{low, allPass(b), "policy may only raise"}, {partial, allPass(four), "needs a weight or a recorded exclusion"}} {
		if _, err := tryScore(t, tc.r, resOf(tc.st), passing()); err == nil || !strings.Contains(err.Error(), tc.want) {
			t.Fatalf("want %q, got %v", tc.want, err)
		}
	}
}

func writeJSON(t *testing.T, dir, name string, v any) string {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatal(err)
	}
	p := filepath.Join(dir, name)
	if err := os.WriteFile(p, b, 0o644); err != nil {
		t.Fatal(err)
	}
	return p
}

// pinned sets the results' rubric_digest to the digest of the rubric file.
func pinned(t *testing.T, rubricPath string, results map[string]any) map[string]any {
	t.Helper()
	d, err := ovio.SHA256File(rubricPath)
	if err != nil {
		t.Fatal(err)
	}
	results["rubric_digest"] = d
	return results
}

func runCLI(args ...string) (int, string, string) {
	var o, e bytes.Buffer
	code := Run(args, &o, &e)
	return code, o.String(), e.String()
}

func TestCompare(t *testing.T) {
	var excl []any
	for _, d := range doms[2:] {
		excl = append(excl, map[string]any{"domain": d, "reason": "out of scope", "evidence": []string{"EV-x"}})
	}
	r := map[string]any{"schema": "overhaul.rubric/1", "rev": 1, "lanes": []string{"backend"},
		"domains": map[string]any{"correctness": 50, "security": 50}, "domain_exclusions": excl,
		"weight_deviations": []any{
			map[string]any{"domain": "correctness", "reason": "two-domain library", "approval": "AP-1"},
			map[string]any{"domain": "security", "reason": "two-domain library", "approval": "AP-1"}},
		"controls": []map[string]any{ctl(1, "correctness", 5), ctl(2, "correctness", 5), ctl(3, "security", 5)}}
	dir := t.TempDir()
	rp := writeJSON(t, dir, "r.json", r)
	code, stdout, stderr := runCLI("compare", "--rubric", rp,
		"--baseline", writeJSON(t, dir, "a.json", pinned(t, rp, resOf(map[string]string{"C-001": "PASS", "C-002": "UNKNOWN", "C-003": "PASS"}))),
		"--candidate", writeJSON(t, dir, "b.json", pinned(t, rp, resOf(map[string]string{"C-001": "FAIL", "C-002": "PASS", "C-003": "PASS"}))))
	if code != 0 {
		t.Fatalf("exit %d: %s", code, stderr)
	}
	out := norm(t, json.RawMessage(stdout))
	if get(out, "baseline_Q.exact") != "15/2" || get(out, "candidate_Q.exact") != "15/2" {
		t.Fatalf("Q %v / %v", out["baseline_Q"], out["candidate_Q"])
	}
	rows := ovio.List(out["transitions"])
	if len(rows) != 2 ||
		get(rows[0], "control") != "C-001" || get(rows[0], "kind") != "regression-or-new-failure" ||
		get(rows[1], "control") != "C-002" || get(rows[1], "kind") != "evidence-only" {
		t.Fatalf("transitions %v", rows)
	}
}

func TestCLIOutWritesScorecard(t *testing.T) {
	dir := t.TempDir()
	b := base()
	outPath := filepath.Join(dir, "scorecard.json")
	rp := writeJSON(t, dir, "r.json", rubricOf(b))
	code, stdout, stderr := runCLI("--rubric", rp,
		"--results", writeJSON(t, dir, "s.json", pinned(t, rp, resOf(allPass(b)))),
		"--gates", writeJSON(t, dir, "g.json", passing()), "--out", outPath)
	if code != 0 || stdout != "MEETS_TARGETS -> "+outPath+"\n" {
		t.Fatalf("exit %d stdout %q stderr %q", code, stdout, stderr)
	}
	card, err := ovio.LoadObject(outPath)
	if err != nil {
		t.Fatal(err)
	}
	if get(card, "schema") != "overhaul.scorecard/1" || get(card, "readiness_verdict") != "MEETS_TARGETS" ||
		len(get(card, "inputs.rubric_sha256")) != 64 {
		t.Fatalf("scorecard %v", card)
	}
}

func TestCLIInvalidInputExits2(t *testing.T) {
	dir := t.TempDir()
	b := base()
	bad := rubricOf(b)
	bad["schema"] = "overhaul.rubric/0"
	rubric := writeJSON(t, dir, "r.json", rubricOf(b))
	stale := resOf(allPass(b))
	stale["rubric_digest"] = strings.Repeat("0", 64)
	for name, args := range map[string][]string{
		"bad rubric": {"--rubric", writeJSON(t, dir, "bad.json", bad), "--results", writeJSON(t, dir, "s.json", resOf(allPass(b))),
			"--gates", writeJSON(t, dir, "g.json", passing())},
		"stale digest": {"--rubric", rubric, "--results", writeJSON(t, dir, "stale.json", stale),
			"--gates", filepath.Join(dir, "g.json")},
	} {
		code, stdout, stderr := runCLI(args...)
		if code != 2 || stdout != "" || !strings.HasPrefix(stderr, "ERROR: ") {
			t.Fatalf("%s: exit %d stdout %q stderr %q", name, code, stdout, stderr)
		}
	}
}

// A domain weight off the default table needs a recorded deviation with an approval.
func TestDomainWeightDeviation(t *testing.T) {
	b := base()
	r := rubricOf(b)
	r["domains"].(map[string]any)["security"] = 30
	if _, err := tryScore(t, r, resOf(allPass(b)), passing()); err == nil || !strings.Contains(err.Error(), "weight_deviations") {
		t.Fatalf("undeclared deviation accepted: %v", err)
	}
	r["weight_deviations"] = []any{map[string]any{"domain": "security", "reason": "payments service"}}
	if _, err := tryScore(t, r, resOf(allPass(b)), passing()); err == nil || !strings.Contains(err.Error(), "approval reference") {
		t.Fatalf("deviation without approval accepted: %v", err)
	}
	dup := map[string]any{"domain": "security", "reason": "payments service", "approval": "AP-1"}
	r["weight_deviations"] = []any{dup, dup}
	if _, err := tryScore(t, r, resOf(allPass(b)), passing()); err == nil || !strings.Contains(err.Error(), "unknown or repeated") {
		t.Fatalf("repeated deviation accepted: %v", err)
	}
	r["weight_deviations"] = []any{dup}
	mustScore(t, r, resOf(allPass(b)), passing())
}

// A performance PASS from static inspection earns nothing.
func TestStaticPerformancePassIsUnknown(t *testing.T) {
	b := base()
	b[3] = maps.Clone(b[3])
	b[3]["evaluation"] = "static"
	out := mustScore(t, rubricOf(b), resOf(allPass(b)), passing())
	if get(out, "status_by_control."+b[3]["id"].(string)) != "UNKNOWN" || get(out, "readiness_verdict") != "NOT_READY" {
		t.Fatalf("static performance PASS counted: %v", out["status_by_control"])
	}
}

// Results without the rubric digest, or compared against another rubric, are invalid.
func TestRubricDigestRequired(t *testing.T) {
	dir := t.TempDir()
	b := base()
	rp := writeJSON(t, dir, "r.json", rubricOf(b))
	other := rubricOf(b)
	other["rev"] = 2
	op := writeJSON(t, dir, "other.json", other)
	g := writeJSON(t, dir, "g.json", passing())
	for name, args := range map[string][]string{
		"score without digest": {"--rubric", rp, "--results", writeJSON(t, dir, "s.json", resOf(allPass(b))), "--gates", g},
		"compare other rubric": {"compare", "--rubric", rp,
			"--baseline", writeJSON(t, dir, "a.json", pinned(t, rp, resOf(allPass(b)))),
			"--candidate", writeJSON(t, dir, "c.json", pinned(t, op, resOf(allPass(b))))},
	} {
		if code, _, stderr := runCLI(args...); code != 2 || !strings.Contains(stderr, "rubric") {
			t.Fatalf("%s: exit %d %q", name, code, stderr)
		}
	}
}

// A results document that names no code state is invalid input: exit 2, one
// diagnostic naming the absent field, and no scorecard written.
func TestResultsMustIdentifyScoredState(t *testing.T) {
	dir := t.TempDir()
	b := base()
	rp := writeJSON(t, dir, "r.json", rubricOf(b))
	g := writeJSON(t, dir, "g.json", passing())
	for i, tc := range []struct {
		name   string
		mutate func(map[string]any)
		want   string
	}{
		{"fingerprint absent", func(r map[string]any) { delete(r, "fingerprint") }, "fingerprint"},
		{"fingerprint null", func(r map[string]any) { r["fingerprint"] = nil }, "fingerprint"},
		{"fingerprint empty", func(r map[string]any) { r["fingerprint"] = "" }, "fingerprint"},
		{"fingerprint placeholder", func(r map[string]any) { r["fingerprint"] = "not-a-sha256" }, "fingerprint"},
		{"fingerprint one short", func(r map[string]any) { r["fingerprint"] = fp[:len(fp)-1] }, "fingerprint"},
		{"fingerprint non-hex", func(r map[string]any) { r["fingerprint"] = strings.Repeat("z", 64) }, "fingerprint"},
		{"subject absent", func(r map[string]any) { delete(r, "subject") }, "subject"},
		{"subject empty", func(r map[string]any) { r["subject"] = "" }, "subject"},
		{"subject not a string", func(r map[string]any) { r["subject"] = 12 }, "subject"},
	} {
		res := pinned(t, rp, resOf(allPass(b)))
		tc.mutate(res)
		sp := writeJSON(t, dir, "s.json", res)
		card := filepath.Join(dir, fmt.Sprintf("card-%d.json", i))
		other := "fingerprint"
		if tc.want == "fingerprint" {
			other = "subject"
		}
		code, stdout, stderr := runCLI("--rubric", rp, "--results", sp, "--gates", g, "--out", card)
		switch {
		case code != 2:
			t.Errorf("%s: exit %d, want 2 (stdout %q stderr %q)", tc.name, code, stdout, stderr)
		case stdout != "":
			t.Errorf("%s: published %q", tc.name, stdout)
		case !strings.HasPrefix(stderr, "ERROR: "):
			t.Errorf("%s: %q is not a tool diagnostic", tc.name, stderr)
		case !strings.Contains(stderr, tc.want):
			t.Errorf("%s: %q does not name %s", tc.name, stderr, tc.want)
		case strings.Contains(stderr, other):
			t.Errorf("%s: %q names %s, not %s", tc.name, stderr, other, tc.want)
		case strings.Contains(stderr, "rubric_digest"):
			t.Errorf("%s: the digest check fired, not the field check: %q", tc.name, stderr)
		}
		if _, err := os.Stat(card); err == nil {
			t.Errorf("%s: wrote a scorecard for an unidentified code state", tc.name)
		}
	}
}

// The exit 2 above is the absent field, not scoring: identified results score,
// and a failing control is a scorecard at exit 0 carrying the validated
// fingerprint verbatim.
func TestIdentifiedResultsStillScore(t *testing.T) {
	dir := t.TempDir()
	b := base()
	rp := writeJSON(t, dir, "r.json", rubricOf(b))
	g := writeJSON(t, dir, "g.json", passing())
	card := filepath.Join(dir, "card.json")
	code, stdout, stderr := runCLI("--rubric", rp, "--results", writeJSON(t, dir, "s.json", pinned(t, rp, resOf(allPass(b)))),
		"--gates", g, "--out", card)
	if code != 0 || stderr != "" {
		t.Fatalf("identified results: exit %d stdout %q stderr %q", code, stdout, stderr)
	}
	c, err := ovio.LoadObject(card)
	if err != nil {
		t.Fatal(err)
	}
	if get(c, "fingerprint") != fp || get(c, "subject") != "candidate" || get(c, "readiness_verdict") != "MEETS_TARGETS" {
		t.Fatalf("scorecard %v", c)
	}
	st := allPass(b)
	st[b[0]["id"].(string)] = "FAIL"
	code, _, stderr = runCLI("--rubric", rp, "--results", writeJSON(t, dir, "f.json", pinned(t, rp, resOf(st))), "--gates", g)
	if code != 0 || stderr != "" {
		t.Fatalf("failing control: exit %d stderr %q", code, stderr)
	}
	out := mustScore(t, rubricOf(b), resOf(st), passing())
	if get(out, "readiness_verdict") != "NOT_READY" || get(out, "fingerprint") != fp {
		t.Fatalf("failing control: verdict %s fingerprint %s", get(out, "readiness_verdict"), get(out, "fingerprint"))
	}
}

// compare scores both arms through the same check, so an arm that identifies no
// code state is rejected with the arm named.
func TestCompareArmsRequireScoredState(t *testing.T) {
	dir := t.TempDir()
	b := base()
	rp := writeJSON(t, dir, "r.json", rubricOf(b))
	arm := func(name string, identified bool) string {
		res := pinned(t, rp, resOf(allPass(b)))
		if !identified {
			delete(res, "fingerprint")
		}
		return writeJSON(t, dir, name, res)
	}
	for _, tc := range []struct {
		arm       string
		baseline  string
		candidate string
	}{
		{"baseline", arm("blank-baseline.json", false), arm("ok-candidate.json", true)},
		{"candidate", arm("ok-baseline.json", true), arm("blank-candidate.json", false)},
	} {
		code, stdout, stderr := runCLI("compare", "--rubric", rp, "--baseline", tc.baseline, "--candidate", tc.candidate)
		if code != 2 || stdout != "" || !strings.HasPrefix(stderr, "ERROR: "+tc.arm+": ") ||
			!strings.Contains(stderr, "fingerprint") {
			t.Fatalf("%s arm: exit %d stdout %q stderr %q", tc.arm, code, stdout, stderr)
		}
	}
}
