package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"testing"
)

// ---------------------------------------------------------------------------
// Test harness
// ---------------------------------------------------------------------------

var binPath string

func TestMain(m *testing.M) {
	dir, err := os.MkdirTemp("", "clipstudio-bin-")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	binPath = filepath.Join(dir, "clipstudio")
	build := exec.Command("go", "build", "-o", binPath, ".")
	build.Stderr = os.Stderr
	if err := build.Run(); err != nil {
		fmt.Fprintln(os.Stderr, "cannot build test binary:", err)
		os.Exit(1)
	}
	code := m.Run()
	os.RemoveAll(dir)
	os.Exit(code)
}

type cliResult struct {
	stdout, stderr string
	code           int
}

func (r cliResult) all() string { return r.stdout + "\n" + r.stderr }

func runCLI(t *testing.T, args ...string) cliResult {
	t.Helper()
	cmd := exec.Command(binPath, args...)
	var out, errb strings.Builder
	cmd.Stdout = &out
	cmd.Stderr = &errb
	err := cmd.Run()
	res := cliResult{stdout: out.String(), stderr: errb.String()}
	if err != nil {
		var ee *exec.ExitError
		if errors.As(err, &ee) {
			res.code = ee.ExitCode()
		} else {
			t.Fatalf("running %v: %v", args, err)
		}
	}
	return res
}

// writeCast builds a SessionForge-format cast file from (delay, stream, text).
func writeCast(t *testing.T, path, title string, command []string, startedAt string, events [][3]string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatal(err)
	}
	hdr := castHeader{Version: 1, Command: command, StartedAt: startedAt,
		Shell: "/bin/bash", Width: 120, Height: 34, Title: title}
	line, err := json.Marshal(hdr)
	if err != nil {
		t.Fatal(err)
	}
	var b strings.Builder
	b.Write(line)
	b.WriteByte('\n')
	for _, ev := range events {
		payload, _ := json.Marshal(ev[2])
		fmt.Fprintf(&b, "[%s, %q, %s]\n", ev[0], ev[1], payload)
	}
	b.WriteString(`{"exit_code":0,"duration":9.5}` + "\n")
	if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil {
		t.Fatal(err)
	}
}

func mustFindings(t *testing.T, text string) []Finding {
	t.Helper()
	dir := t.TempDir()
	p := filepath.Join(dir, "c.jsonl")
	writeCast(t, p, "t", []string{"sh"}, "2026-08-01T00:00:00Z",
		[][3]string{{"0.100000", "o", text}})
	c, err := loadCast(p)
	if err != nil {
		t.Fatal(err)
	}
	return analyze(c).findings
}

// ---------------------------------------------------------------------------
// The fixture credentials. Every one of these strings must be absent from
// every byte the program ever writes.
// ---------------------------------------------------------------------------

const (
	secAWSID     = "AKIAIOSFODNN7EXAMPLE"
	secAWSSecret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
	secGitHub    = "ghp_16C7e42F292c6912E7710c838347Ae178B4a"
	secGitHubPAT = "github_pat_11ABCDEFG0abcdefghijklmnopqrstuvwxyz1234567890ABCD"
	secSlack     = "xoxb-2404757907-2404757956-Xk8vQm3TpLbNwR7yEd2Zh"
	secGoogle    = "AIzaSyD-9tSrke72PouQMnMX-a7eZSW0jkFMBWY"
	secJWT       = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
	secBearer    = "sk_live_9Xk2Pq7Lm4Rv8Nt1Wz6Yb3Cd5Fg0Hj"
	secConnPass  = "Tr0ub4dor-and-3"
	secAssigned  = "hunter2correcthorse"
	secEntropy   = "xK9mP2vQ8nR4tY7wB1zC5dF3gH6jL0aS"
	secPEMBody   = "MIIEowIBAAKCAQEAyPvRZ8kQ2mJ3nXKcT1wBv7aLdG9hUqYs4eRxNpWmZbC0Ftij"
)

var allSecrets = []string{
	secAWSID, secAWSSecret, secGitHub, secGitHubPAT, secSlack, secGoogle,
	secJWT, secBearer, secConnPass, secAssigned, secEntropy, secPEMBody,
}

// leakyCast writes one cast containing every fixture credential, in the
// stream, in the command line and in the title.
func leakyCast(t *testing.T, path string) {
	t.Helper()
	writeCast(t, path, "deploy with "+secEntropy,
		[]string{"bash", "-c", "./deploy.sh --token=" + secBearer},
		"2026-08-09T09:14:02Z",
		[][3]string{
			{"0.001227", "o", "$ ./deploy.sh\r\n"},
			{"1.102926", "o", "export AWS_ACCESS_KEY_ID=" + secAWSID + "\r\n"},
			{"1.107087", "o", "export AWS_SECRET_ACCESS_KEY=" + secAWSSecret + "\r\n"},
			{"2.010487", "o", "curl -H 'Authorization: Bearer " + secGitHub + "'\r\n"},
			{"2.310487", "o", "GH_PAT=" + secGitHubPAT + "\r\n"},
			{"2.610487", "e", "SLACK_BOT_TOKEN=" + secSlack + "\r\n"},
			{"2.910487", "o", "GOOGLE_KEY=" + secGoogle + "\r\n"},
			{"3.210487", "o", "TOKEN=" + secJWT + "\r\n"},
			{"3.510487", "o", "psql postgres://deployer:" + secConnPass + "@db.internal:5432/app\r\n"},
			{"3.810487", "o", "PASSWORD=" + secAssigned + "\r\n"},
			{"4.110487", "o", "cookie " + secEntropy + "\r\n"},
			{"4.410487", "o", "-----BEGIN RSA PRIVATE KEY-----\n" + secPEMBody + "\n-----END RSA PRIVATE KEY-----\n"},
			{"9.500000", "o", "done\r\n"},
		})
}

// ---------------------------------------------------------------------------
// 1. Named pattern detectors: positive and negative fixtures
// ---------------------------------------------------------------------------

func TestNamedDetectors(t *testing.T) {
	cases := []struct {
		name     string
		detector string
		positive string
		negative string
	}{
		{
			name: "aws access key id", detector: "aws-access-key-id",
			positive: "+ export AWS_ACCESS_KEY_ID=" + secAWSID + "\n",
			// one character short of the 16 required after the prefix
			negative: "placeholder AKIAIOSFODNN7EXAMPL was rejected\n",
		},
		{
			name: "aws access key id (STS)", detector: "aws-access-key-id",
			positive: "session key ASIAY34FZKBOKMUTVV7A issued\n",
			negative: "ASIA is the region grouping we use\n",
		},
		{
			name: "aws secret access key", detector: "aws-secret-access-key",
			positive: "aws_secret_access_key = " + secAWSSecret + "\n",
			// same shape, no aws-secret context word anywhere near it
			negative: "payload digest " + secAWSSecret + "\n",
		},
		{
			name: "github classic token", detector: "github-token",
			positive: "gh auth login --with-token " + secGitHub + "\n",
			negative: "the ghp_ prefix marks a classic token\n",
		},
		{
			name: "github fine-grained pat", detector: "github-token",
			positive: "GH_PAT=" + secGitHubPAT + "\n",
			negative: "see docs for github_pat_ tokens\n",
		},
		{
			name: "slack token", detector: "slack-token",
			positive: "SLACK_BOT_TOKEN=" + secSlack + "\n",
			negative: "the xoxb- prefix identifies a bot token\n",
		},
		{
			name: "google api key", detector: "google-api-key",
			positive: "GOOGLE_MAPS_KEY=" + secGoogle + "\n",
			negative: "keys begin with AIza followed by 35 characters\n",
		},
		{
			name: "jwt", detector: "jwt",
			positive: "Authorization: " + secJWT + "\n",
			// three dotted segments, but the header does not decode to JSON
			negative: "archive at backups.example.com.tar.gz stored\n",
		},
		{
			name: "private key pem", detector: "private-key-pem",
			positive: "-----BEGIN RSA PRIVATE KEY-----\n" + secPEMBody + "\n-----END RSA PRIVATE KEY-----\n",
			negative: "-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAK\n-----END CERTIFICATE-----\n",
		},
		{
			name: "bearer token", detector: "bearer-token",
			positive: "Authorization: Bearer " + secBearer + "\n",
			negative: "A Bearer token is required for this endpoint\n",
		},
		{
			name: "connection string password", detector: "connection-string-password",
			positive: "psql postgres://deployer:" + secConnPass + "@db.internal:5432/app\n",
			negative: "psql postgres://deployer@db.internal:5432/app\n",
		},
		{
			name: "secret assignment", detector: "secret-assignment",
			positive: "PASSWORD=" + secAssigned + "\n",
			negative: "PASSWORD=$DB_PASSWORD\n",
		},
		{
			name: "high entropy string", detector: "high-entropy-string",
			positive: "session cookie " + secEntropy + " set\n",
			negative: "request id 550e8400-e29b-41d4-a716-446655440000 served\n",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name+"/positive", func(t *testing.T) {
			got := mustFindings(t, tc.positive)
			if len(got) == 0 {
				t.Fatalf("no findings at all for %s", tc.detector)
			}
			found := false
			for _, f := range got {
				if f.Detector == tc.detector {
					found = true
				}
			}
			if !found {
				var names []string
				for _, f := range got {
					names = append(names, f.Detector)
				}
				t.Errorf("want detector %q, got %v", tc.detector, names)
			}
		})
		t.Run(tc.name+"/negative", func(t *testing.T) {
			got := mustFindings(t, tc.negative)
			for _, f := range got {
				if f.Detector == tc.detector {
					t.Errorf("detector %q fired on a negative fixture (masked %q)", tc.detector, f.Masked)
				}
			}
		})
	}
}

// ---------------------------------------------------------------------------
// 2. Entropy scoring and its boundaries
// ---------------------------------------------------------------------------

func TestShannonEntropy(t *testing.T) {
	cases := []struct {
		name   string
		in     string
		lo, hi float64
	}{
		{"empty", "", 0, 0},
		{"single char", "a", 0, 0},
		{"one repeated char", strings.Repeat("a", 64), 0, 0},
		{"two chars evenly", "abababab", 0.99, 1.01},
		{"four chars evenly", "abcdabcdabcdabcd", 1.99, 2.01},
		{"sixteen distinct", "0123456789abcdef", 3.99, 4.01},
		{"english prose", "the connection pool was exhausted during the burst", 3.4, 4.2},
		{"english pangram", "the quick brown fox jumps over the lazy dog", 4.2, 4.5},
		{"random-looking base64", secEntropy, 4.4, 5.1},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := shannonEntropy(tc.in)
			if got < tc.lo || got > tc.hi {
				t.Errorf("shannonEntropy(%q) = %.4f, want between %.2f and %.2f", tc.in, got, tc.lo, tc.hi)
			}
		})
	}
}

func TestEntropyDetectorBoundaries(t *testing.T) {
	// A 19-character token is below the length floor no matter how random.
	short := "xK9mP2vQ8nR4tY7wB1z" // 19
	if len(short) != entropyMinLength-1 {
		t.Fatalf("fixture length changed: %d", len(short))
	}
	if got := entropyHits("cookie " + short + " set\n"); got != 0 {
		t.Errorf("token one byte below the length floor produced %d entropy findings", got)
	}
	// One character longer clears the floor and is reported.
	long := short + "C"
	if len(long) != entropyMinLength {
		t.Fatalf("fixture length changed: %d", len(long))
	}
	if got := entropyHits("cookie " + long + " set\n"); got != 1 {
		t.Errorf("token exactly at the length floor produced %d entropy findings, want 1", got)
	}
	// Long enough, but the entropy is far too low: a repeated pattern.
	lowEntropy := strings.Repeat("abcd", 9) // 36 chars, entropy 2.0
	if e := shannonEntropy(lowEntropy); e >= entropyThreshold {
		t.Fatalf("fixture entropy changed: %.3f", e)
	}
	if got := entropyHits("cookie " + lowEntropy + " set\n"); got != 0 {
		t.Errorf("low-entropy token produced %d entropy findings", got)
	}
	// Mixed-class rule: 20 chars of lower-case only stays below the strong
	// gate and below the long-string length, so it is not reported.
	lowerOnly := "qkzvxjrwmbnthdgplsfc" // 20 distinct lower-case letters
	if got := entropyHits("cookie " + lowerOnly + " set\n"); got != 0 {
		t.Errorf("20-char lower-case-only token produced %d entropy findings", got)
	}
}

func entropyHits(text string) int {
	n := 0
	for _, s := range scanText(text) {
		if s.detector == "high-entropy-string" {
			n++
		}
	}
	return n
}

func TestDenoisePass(t *testing.T) {
	cases := []struct {
		name string
		text string
	}{
		{"uuid", "request id 550e8400-e29b-41d4-a716-446655440000 served\n"},
		{"git commit sha", "commit 3f2a9c1d8e4b7a6f5c0d9e8b7a6f5c4d3e2b1a09\n"},
		{"sha256 digest", "image sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n"},
		{"md5 in context", "md5 checksum 9e107d9d372bb6826bd81d3542a419d6\n"},
		{"base64 of plain text", "payload VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==\n"},
		{"filesystem path", "hot path /usr/local/lib/python3.11/site-packages/urllib3/util/retry.py\n"},
		{"all numeric", "offset 1234567890123456789012345678901234567890 bytes\n"},
		{"repeated character", "progress " + strings.Repeat("A", 40) + "\n"},
		{"already masked", "PASSWORD=" + strings.Repeat("*", 24) + "\n"},
		{"iso timestamp", "at 2026-08-11T04:10:29.123456789Z the pod restarted\n"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := mustFindings(t, tc.text); len(got) != 0 {
				t.Errorf("de-noising failed: %d findings (%s)", len(got), got[0].Detector)
			}
		})
	}
}

// ---------------------------------------------------------------------------
// 3. Measured detector accuracy on a broad, realistic corpus.
//
// This test REPORTS false positives and false negatives rather than pretending
// there are none. It fails only if accuracy drops below the floor documented
// in README.txt, so a regression is caught without anybody claiming the
// detector is perfect.
// ---------------------------------------------------------------------------

var accuracyPositives = []string{
	"+ export AWS_ACCESS_KEY_ID=" + secAWSID + "\n",
	"session key ASIAY34FZKBOKMUTVV7A issued at 09:14\n",
	"aws_secret_access_key = " + secAWSSecret + "\n",
	"gh auth login --with-token " + secGitHub + "\n",
	"GH_PAT=" + secGitHubPAT + "\n",
	"SLACK_BOT_TOKEN=" + secSlack + "\n",
	"xoxp-1234567890-0987654321-abcdefghijklmnopqrst\n",
	"GOOGLE_MAPS_KEY=" + secGoogle + "\n",
	"Authorization: " + secJWT + "\n",
	"-----BEGIN OPENSSH PRIVATE KEY-----\n" + secPEMBody + "\n-----END OPENSSH PRIVATE KEY-----\n",
	"curl -H 'Authorization: Bearer " + secBearer + "'\n",
	"psql postgres://deployer:" + secConnPass + "@db.internal:5432/app\n",
	"mongodb://admin:S3cr3t99xyz@cluster0.mongodb.net/test\n",
	"PASSWORD=" + secAssigned + "\n",
	`export APP_SECRET="J8fk2LmQp9XvR4tZ"` + "\n",
	"TOKEN=aB3dE5fG7hJ9kL1mN3pQ5rS7\n",
	"session cookie " + secEntropy + " set\n",
	"api_key: 4f8a2b9c1e6d7350aF2bC9dE\n",
	"DATABASE_PASSWORD=pV7!kQ2m#Ln9\n",
	"client_secret=GOCSPX-1a2B3c4D5e6F7g8H9i0JkLmNo\n",
}

var accuracyNegatives = []string{
	"PWD=/home/user/project\n",
	"AWS_CREDENTIALS=/etc/aws/credentials\n",
	"commit 3f2a9c1d8e4b7a6f5c0d9e8b7a6f5c4d3e2b1a09\n",
	"request id 550e8400-e29b-41d4-a716-446655440000 served\n",
	"image sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n",
	"payload VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==\n",
	"hot path /usr/local/lib/python3.11/site-packages/urllib3/util/retry.py:412\n",
	"Content-Type: application/json; charset=utf-8\n",
	"placeholder AKIAIOSFODNN7EXAMPL rejected\n",
	"at 2026-08-11T04:10:29.123456789Z the pod restarted\n",
	"npm WARN deprecated request@2.88.2: request has been deprecated\n",
	"pulling docker.io/library/postgres:16.2-alpine\n",
	"GET https://example.com/path?q=hello&page=2 200\n",
	"TOKENIZER=wordpiece\n",
	"-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAK\n-----END CERTIFICATE-----\n",
	"psql postgres://svc@db.internal:5432/app\n",
	"total 48 drwxr-xr-x 12 user staff 384 Aug 11 04:10 .\n",
	"export EDITOR=vim\n",
	"PASSWORD=$DB_PASSWORD\n",
	"api_key: <your-api-key-here>\n",
	"offset 1234567890123456789012345678901234567890 bytes\n",
	"progress " + strings.Repeat("A", 40) + "\n",
	"checkout refs/heads/feature/add-secret-scanning\n",
	"A Bearer token is required for this endpoint\n",
	`{"login":"deploybot","id":48291045,"node_id":"MDQ6VXNlcjQ4MjkxMDQ1"}` + "\n",
	"Downloading https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz\n",
	"route                     p50     p95     p99\n",
	"verify digest 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\n",
	"restore complete, 812403 rows in 41.2 GiB\n",
	"GOMODCACHE=/home/user/go/pkg/mod GOPROXY=https://proxy.golang.org\n",
}

// Documented accuracy floor. See the SCOPE section of README.txt.
const (
	minRecall    = 0.85
	maxFPRate    = 0.15
	accuracyNote = "detector accuracy is measured, not assumed"
)

func TestDetectorAccuracy(t *testing.T) {
	var fn, fp int
	for _, s := range accuracyPositives {
		if len(scanText(s)) == 0 {
			fn++
			t.Logf("FALSE NEGATIVE: %q", strings.TrimRight(s, "\n"))
		}
	}
	for _, s := range accuracyNegatives {
		if hits := scanText(s); len(hits) > 0 {
			fp++
			t.Logf("FALSE POSITIVE: %q -> %s", strings.TrimRight(s, "\n"), hits[0].detector)
		}
	}
	tp := len(accuracyPositives) - fn
	tn := len(accuracyNegatives) - fp
	recall := float64(tp) / float64(len(accuracyPositives))
	fpRate := float64(fp) / float64(len(accuracyNegatives))
	t.Logf("MEASURED ACCURACY (%s)", accuracyNote)
	t.Logf("  positives : %d, true positives %d, FALSE NEGATIVES %d, recall %.3f",
		len(accuracyPositives), tp, fn, recall)
	t.Logf("  negatives : %d, true negatives %d, FALSE POSITIVES %d, fp-rate %.3f",
		len(accuracyNegatives), tn, fp, fpRate)
	if recall < minRecall {
		t.Errorf("recall %.3f is below the documented floor %.2f", recall, minRecall)
	}
	if fpRate > maxFPRate {
		t.Errorf("false-positive rate %.3f is above the documented ceiling %.2f", fpRate, maxFPRate)
	}
}

// ---------------------------------------------------------------------------
// 3b. HELD-OUT corpus.
//
// The corpus above was written alongside the detectors, so scoring well on it
// proves little. This second corpus is deliberately made of credential
// families ClipStudio has no named rule for and of real terminal output that
// looks credential-shaped, and the detectors were NOT tuned against it. Its
// numbers are the ones quoted in README.txt. The floor is loose on purpose:
// this test exists to MEASURE, and to catch a catastrophic regression, not to
// be argued down to zero.
// ---------------------------------------------------------------------------

var heldOutPositives = []string{
	"STRIPE_KEY=sk_live_51H8xYzABCdefGHIjklMNOpqrSTUvwxYZ0123456789abcdef\n",
	"TWILIO_AUTH_TOKEN=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\n",
	"SENDGRID=SG.x9AbCdEfGhIjKlMnOpQrSt.uVwXyZ0123456789AbCdEfGhIjKlMnOpQr\n",
	"npm config set //registry.npmjs.org/:_authToken=npm_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789\n",
	"AZURE_STORAGE_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==\n",
	"Authorization: Basic YWRtaW46c3VwZXJzZWNyZXQxMjM=\n",
	"mysql -u root -phunter2Sw0rdf1sh orders\n",
	"curl -u admin:s3cr3tp4ssw0rd https://api.internal/v1/health\n",
	"export DB_PASS=Qw3rty2026Zx\n",
	"discord token MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs\n",
	"PGPASSWORD=hunter2Sw0rdf1sh psql -h db -U app\n",
	"  aws_access_key_id: AKIA1234567890ABCDEF\n",
	"heroku config:set SECRET_KEY_BASE=8f14e45fceea167a5a36dedd4bea2543\n",
	"-----BEGIN PGP PRIVATE KEY BLOCK-----\nlQOYBGX1234ABCDEFghijkLMNOP\n-----END PGP PRIVATE KEY BLOCK-----\n",
	"kubectl create secret generic db --from-literal=password=S3cur3P4ssw0rd\n",
	"DIGITALOCEAN_TOKEN=dop_v1_9f8e7d6c5b4a39281706152433425160708192a3b4c5d6e7\n",
	"redis-cli -a Sup3rS3cr3tR3d1s -h cache.internal ping\n",
	"OPENAI_API_KEY=sk-proj-A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8S9t0\n",
	"htpasswd -bc .htpasswd admin Str0ngP@ssw0rd2026\n",
	"vault write secret/db password=Zx9Kq2Mw7Lp4Nv8Tb1\n",
}

var heldOutNegatives = []string{
	"go: downloading github.com/stretchr/testify v1.9.0\n",
	"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  release.tar.gz\n",
	"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7hZvKpQmXnRtLbW3dYfGcJs2eUiOaTxZ user@host\n",
	"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\n-----END PUBLIC KEY-----\n",
	"docker image id sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\n",
	"X-Request-Id: 7f3e9a1b-4c2d-4e5f-8a9b-0c1d2e3f4a5b\n",
	"npm notice integrity sha512-Xk8vQm3TpLbNwR7yEd2ZhQmXnRtLbW3dYfGcJs2eUiOaTx==\n",
	`ETag: "d41d8cd98f00b204e9800998ecf8427e"` + "\n",
	"AWS_REGION=eu-west-1 AWS_DEFAULT_OUTPUT=json\n",
	"--- PASS: TestRetainApplyMovesAndNeverUnlinks/nested_subtest (0.02s)\n",
	"Content-Security-Policy: script-src 'nonce-2726c7f26c'\n",
	"BUILD_ID=2026.8.11-a3f9c2e1b0d4\n",
	"PYTHONPATH=/opt/app:/opt/app/vendor:/usr/lib/python3.11\n",
	"curl -sSL https://get.docker.com | sh\n",
	"TRACEPARENT=00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\n",
	"IMAGE=ghcr.io/acme/api@sha256:abcd1234ef567890abcd1234ef567890abcd1234ef567890abcd1234ef567890\n",
	"perf: 0.9234821 seconds elapsed, 41203 allocations\n",
	"drwxr-xr-x  12 deploy staff   384 Aug 11 04:10 node_modules\n",
	"Cloning into 'infrastructure-as-code-templates'...\n",
	"HTTP/2 200 date: Tue, 11 Aug 2026 04:10:29 GMT server: nginx/1.25.3\n",
	"user_id=48291045 org_id=90210 plan=enterprise seats=250\n",
	"COMMIT_RANGE=a3f9c2e1b0d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8..b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9\n",
	"warning: LF will be replaced by CRLF in src/components/DataTable.tsx\n",
	"Listening on http://0.0.0.0:8080 (press CTRL+C to quit)\n",
	"S3_BUCKET=acme-prod-artifacts-eu-west-1-20260811\n",
	"KUBECONFIG=/home/deploy/.kube/config CONTEXT=prod-eu-west-1\n",
	"Successfully tagged registry.internal/acme/api:2026.8.11-a3f9c2e\n",
	"apt-get install -y --no-install-recommends ca-certificates curl gnupg\n",
	"Test coverage: 87.4% of statements in ./internal/...\n",
	"free -h: Mem: 15Gi used 8.2Gi free 2.1Gi shared 1.0Gi buff/cache 5.4Gi\n",
}

// Loose floor: this test measures and guards against collapse. The real
// numbers are in the log output and in README.txt.
const (
	heldOutMinRecall = 0.50
	heldOutMaxFPRate = 0.30
)

func TestMeasuredAccuracyOnHeldOutCorpus(t *testing.T) {
	var fn, fp int
	for _, s := range heldOutPositives {
		if len(scanText(s)) == 0 {
			fn++
			t.Logf("HELD-OUT FALSE NEGATIVE: %q", strings.TrimRight(s, "\n"))
		}
	}
	for _, s := range heldOutNegatives {
		if hits := scanText(s); len(hits) > 0 {
			fp++
			t.Logf("HELD-OUT FALSE POSITIVE: %q -> %s", strings.TrimRight(s, "\n"), hits[0].detector)
		}
	}
	tp := len(heldOutPositives) - fn
	tn := len(heldOutNegatives) - fp
	recall := float64(tp) / float64(len(heldOutPositives))
	fpRate := float64(fp) / float64(len(heldOutNegatives))
	t.Logf("HELD-OUT MEASURED ACCURACY")
	t.Logf("  positives : %d, true positives %d, FALSE NEGATIVES %d, recall %.3f (miss rate %.3f)",
		len(heldOutPositives), tp, fn, recall, 1-recall)
	t.Logf("  negatives : %d, true negatives %d, FALSE POSITIVES %d, fp-rate %.3f",
		len(heldOutNegatives), tn, fp, fpRate)
	if recall < heldOutMinRecall {
		t.Errorf("held-out recall %.3f collapsed below %.2f", recall, heldOutMinRecall)
	}
	if fpRate > heldOutMaxFPRate {
		t.Errorf("held-out false-positive rate %.3f rose above %.2f", fpRate, heldOutMaxFPRate)
	}
}

// ---------------------------------------------------------------------------
// 4. THE IMPORTANT TEST: no output path ever emits a secret in the clear.
// ---------------------------------------------------------------------------

func TestNoOutputPathLeaksASecret(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	leakyCast(t, filepath.Join(lib, "deploy.jsonl"))
	leakyCast(t, filepath.Join(lib, "nested", "again.jsonl"))
	idxFile := filepath.Join(dir, "library.json")
	shareDir := filepath.Join(dir, "share")
	ledger := filepath.Join(dir, "ledger.jsonl")

	invocations := [][]string{
		{"index", "--lib", lib, "--index", idxFile},
		{"index", "--lib", lib, "--index", idxFile, "--json"},
		{"list", "--index", idxFile},
		{"list", "--index", idxFile, "--json"},
		{"list", "--lib", lib, "--json"},
		{"search", "deploy", "--index", idxFile},
		{"search", "deploy", "--index", idxFile, "--json"},
		{"search", "export", "--lib", lib},
		// Searching FOR each secret must not echo it back either.
		{"search", secAWSID, "--lib", lib},
		{"search", secGitHub, "--lib", lib, "--json"},
		{"search", secConnPass, "--lib", lib},
		{"search", secEntropy, "--lib", lib, "--json"},
		{"scan", "--lib", lib},
		{"scan", "--lib", lib, "--json"},
		{"scan", filepath.Join(lib, "deploy.jsonl")},
		{"scan", filepath.Join(lib, "deploy.jsonl"), "--json"},
		{"redact", "--lib", lib, "--out-dir", shareDir},
		{"redact", "--lib", lib, "--out-dir", shareDir, "--force", "--json"},
		{"retain", "--lib", lib, "--older-than", "1d", "--ledger", ledger},
		{"retain", "--lib", lib, "--older-than", "1d", "--ledger", ledger, "--json"},
		{"help"},
		{"version"},
		{"bogus-command"},
		{"scan"},
		{"retain", "--lib", lib},
	}

	var combined strings.Builder
	for _, args := range invocations {
		r := runCLI(t, args...)
		combined.WriteString(r.all())
		argv := strings.Join(args, " ")
		for _, s := range allSecrets {
			// A secret the CALLER typed on the command line is the caller's
			// own input, not library content; search echoes its query term
			// back. Everything the program learned from the library itself
			// must never appear.
			if strings.Contains(argv, s) {
				continue
			}
			if strings.Contains(r.all(), s) {
				t.Errorf("command %v leaked a secret in cleartext (%s...)", args, s[:6])
			}
		}
	}

	// Every artefact the program wrote must also be free of the secrets -
	// except the ORIGINALS, which it must never have modified.
	for _, artefact := range []string{idxFile, ledger, shareDir} {
		_ = filepath.WalkDir(artefact, func(p string, d fs.DirEntry, err error) error {
			if err != nil || (d != nil && d.IsDir()) {
				return nil
			}
			data, err := os.ReadFile(p)
			if err != nil {
				return nil
			}
			for _, s := range allSecrets {
				if strings.Contains(string(data), s) {
					t.Errorf("written artefact %s contains a secret in cleartext (%s...)", p, s[:6])
				}
			}
			return nil
		})
	}

	// Sanity check the check: the ORIGINAL clip still has every secret, so the
	// absence above is redaction working, not the fixtures being empty.
	orig, err := os.ReadFile(filepath.Join(lib, "deploy.jsonl"))
	if err != nil {
		t.Fatal(err)
	}
	for _, s := range allSecrets {
		if !strings.Contains(string(orig), s) {
			t.Fatalf("fixture is broken: original clip does not contain %s...", s[:6])
		}
	}
	if !strings.Contains(combined.String(), "findings") {
		t.Fatalf("fixture is broken: no command produced a scan report")
	}
}

// ---------------------------------------------------------------------------
// 5. Redaction: timing preserved exactly, output re-scans clean, input intact
// ---------------------------------------------------------------------------

func TestRedactPreservesTimingAndRescansClean(t *testing.T) {
	dir := t.TempDir()
	in := filepath.Join(dir, "in.jsonl")
	out := filepath.Join(dir, "out.jsonl")
	leakyCast(t, in)

	before := fileSHA(t, in)
	beforeBytes, _ := os.ReadFile(in)

	r := runCLI(t, "redact", in, "--out", out)
	if r.code != 0 {
		t.Fatalf("redact exited %d\n%s", r.code, r.all())
	}
	if !strings.Contains(r.stdout, "preserved exactly") {
		t.Errorf("redact did not report preserved timing:\n%s", r.stdout)
	}
	if !strings.Contains(r.stdout, "clean (0 findings remain)") {
		t.Errorf("redact did not report a clean re-scan:\n%s", r.stdout)
	}

	if after := fileSHA(t, in); after != before {
		t.Errorf("the original clip was modified: %s -> %s", before, after)
	}
	afterBytes, _ := os.ReadFile(in)
	if string(beforeBytes) != string(afterBytes) {
		t.Error("the original clip's bytes changed")
	}

	src, err := loadCast(in)
	if err != nil {
		t.Fatal(err)
	}
	dst, err := loadCast(out)
	if err != nil {
		t.Fatal(err)
	}
	if len(src.Events) != len(dst.Events) {
		t.Fatalf("event count changed: %d -> %d", len(src.Events), len(dst.Events))
	}
	for i := range src.Events {
		if src.Events[i].RawTime != dst.Events[i].RawTime {
			t.Errorf("event %d timestamp token changed: %s -> %s", i, src.Events[i].RawTime, dst.Events[i].RawTime)
		}
		if src.Events[i].Time != dst.Events[i].Time {
			t.Errorf("event %d time changed: %v -> %v", i, src.Events[i].Time, dst.Events[i].Time)
		}
		if src.Events[i].Stream != dst.Events[i].Stream {
			t.Errorf("event %d stream changed", i)
		}
		if len(src.Events[i].Data) != len(dst.Events[i].Data) {
			t.Errorf("event %d payload length changed: %d -> %d (mask must be same-length)",
				i, len(src.Events[i].Data), len(dst.Events[i].Data))
		}
	}
	if src.Footer == nil || dst.Footer == nil || *src.Footer != *dst.Footer {
		t.Error("footer changed")
	}
	if src.duration() != dst.duration() {
		t.Errorf("duration changed: %v -> %v", src.duration(), dst.duration())
	}

	// The redacted copy really is clean when re-analysed in-process.
	if got := analyze(dst).findings; len(got) != 0 {
		t.Errorf("redacted output still has %d findings, first is %s", len(got), got[0].Detector)
	}
	// And it really did find something to redact in the first place.
	if got := analyze(src).findings; len(got) < 10 {
		t.Errorf("fixture is broken: only %d findings in the leaky cast", len(got))
	}
}

func TestRedactRefusesToOverwriteTheOriginal(t *testing.T) {
	dir := t.TempDir()
	in := filepath.Join(dir, "in.jsonl")
	leakyCast(t, in)
	before := fileSHA(t, in)

	r := runCLI(t, "redact", in, "--out", in)
	if r.code == 0 {
		t.Error("redact onto the input should fail")
	}
	if after := fileSHA(t, in); after != before {
		t.Error("the original was modified by a refused redaction")
	}
}

func TestRedactRefusesToClobberWithoutForce(t *testing.T) {
	dir := t.TempDir()
	in := filepath.Join(dir, "in.jsonl")
	out := filepath.Join(dir, "out.jsonl")
	leakyCast(t, in)
	if err := os.WriteFile(out, []byte("precious\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	r := runCLI(t, "redact", in, "--out", out)
	if r.code == 0 {
		t.Error("redact over an existing file should fail without --force")
	}
	data, _ := os.ReadFile(out)
	if string(data) != "precious\n" {
		t.Error("existing output was clobbered without --force")
	}
	if r2 := runCLI(t, "redact", in, "--out", out, "--force"); r2.code != 0 {
		t.Errorf("--force should allow the overwrite, exited %d\n%s", r2.code, r2.all())
	}
}

func TestRedactHandlesNonUTF8Payloads(t *testing.T) {
	dir := t.TempDir()
	in := filepath.Join(dir, "in.jsonl")
	out := filepath.Join(dir, "out.jsonl")
	// A base64 event payload carrying invalid UTF-8 alongside a credential.
	body := "PASSWORD=" + secAssigned + "\x80\xfe\x00 binary tail"
	hdr := `{"version":1,"command":["sh"],"started_at":"2026-08-01T00:00:00Z","shell":"/bin/bash","width":80,"height":24,"title":"bin"}`
	enc, _ := json.Marshal(b64(body))
	content := hdr + "\n" + fmt.Sprintf("[0.500000, \"o\", %s, \"b64\"]\n", enc) + `{"exit_code":0,"duration":0.5}` + "\n"
	if err := os.WriteFile(in, []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
	if r := runCLI(t, "redact", in, "--out", out); r.code != 0 {
		t.Fatalf("redact failed on a binary payload: %s", r.all())
	}
	dst, err := loadCast(out)
	if err != nil {
		t.Fatal(err)
	}
	if len(dst.Events) != 1 {
		t.Fatalf("event count %d", len(dst.Events))
	}
	if len(dst.Events[0].Data) != len(body) {
		t.Errorf("payload length changed: %d -> %d", len(body), len(dst.Events[0].Data))
	}
	if strings.Contains(string(dst.Events[0].Data), secAssigned) {
		t.Error("secret survived redaction of a binary payload")
	}
	// The non-secret binary tail is untouched.
	if !strings.Contains(string(dst.Events[0].Data), "\x80\xfe\x00 binary tail") {
		t.Error("binary bytes outside the finding were altered")
	}
}

func b64(s string) string {
	const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
	var out strings.Builder
	data := []byte(s)
	for i := 0; i < len(data); i += 3 {
		var n uint32
		rem := len(data) - i
		n = uint32(data[i]) << 16
		if rem > 1 {
			n |= uint32(data[i+1]) << 8
		}
		if rem > 2 {
			n |= uint32(data[i+2])
		}
		out.WriteByte(alphabet[(n>>18)&63])
		out.WriteByte(alphabet[(n>>12)&63])
		if rem > 1 {
			out.WriteByte(alphabet[(n>>6)&63])
		} else {
			out.WriteByte('=')
		}
		if rem > 2 {
			out.WriteByte(alphabet[n&63])
		} else {
			out.WriteByte('=')
		}
	}
	return out.String()
}

// ---------------------------------------------------------------------------
// 6. Retention: dry run moves nothing, --apply moves and never unlinks
// ---------------------------------------------------------------------------

// fileState maps every regular file under root to its SHA-256.
func fileState(t *testing.T, root string) map[string]string {
	t.Helper()
	out := map[string]string{}
	_ = filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return nil
		}
		rel, _ := filepath.Rel(root, p)
		out[filepath.ToSlash(rel)] = fileSHA(t, p)
		return nil
	})
	return out
}

func contentMultiset(state map[string]string) []string {
	var out []string
	for _, sum := range state {
		out = append(out, sum)
	}
	sort.Strings(out)
	return out
}

func setupRetentionLib(t *testing.T, lib string) {
	t.Helper()
	writeCast(t, filepath.Join(lib, "ancient.jsonl"), "old one", []string{"sh"},
		"2020-01-02T03:04:05Z", [][3]string{{"0.100000", "o", "old output\n"}})
	writeCast(t, filepath.Join(lib, "q1", "older.jsonl"), "older nested", []string{"sh"},
		"2021-06-07T08:09:10Z", [][3]string{{"0.200000", "o", "nested old output\n"}})
	writeCast(t, filepath.Join(lib, "fresh.jsonl"), "brand new", []string{"sh"},
		"2026-08-11T00:00:00Z", [][3]string{{"0.300000", "o", "fresh output\n"}})
}

func TestRetainDryRunMovesNothing(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	ledger := filepath.Join(dir, "ledger.jsonl")
	setupRetentionLib(t, lib)

	before := fileState(t, lib)
	r := runCLI(t, "retain", "--lib", lib, "--older-than", "90d", "--ledger", ledger)
	if r.code != 0 {
		t.Fatalf("retain exited %d\n%s", r.code, r.all())
	}
	if !strings.Contains(r.stdout, "DRY RUN") {
		t.Errorf("dry run not announced:\n%s", r.stdout)
	}
	if !strings.Contains(r.stdout, "would-archive") {
		t.Errorf("nothing selected; the fixture should have selected two clips:\n%s", r.stdout)
	}
	after := fileState(t, lib)
	if len(before) != len(after) {
		t.Fatalf("dry run changed the file count: %d -> %d", len(before), len(after))
	}
	for p, sum := range before {
		if after[p] != sum {
			t.Errorf("dry run moved or changed %s", p)
		}
	}
	// The ledger is written even on a dry run.
	rec := lastLedger(t, ledger)
	if rec["applied"] != false {
		t.Errorf("ledger applied=%v, want false", rec["applied"])
	}
	if rec["clips_selected"].(float64) != 2 {
		t.Errorf("ledger clips_selected=%v, want 2", rec["clips_selected"])
	}
	if rec["clips_moved"].(float64) != 0 {
		t.Errorf("ledger clips_moved=%v, want 0", rec["clips_moved"])
	}
}

func TestRetainApplyMovesAndNeverUnlinks(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	ledger := filepath.Join(dir, "ledger.jsonl")
	setupRetentionLib(t, lib)

	before := fileState(t, lib)
	r := runCLI(t, "retain", "--lib", lib, "--older-than", "90d", "--ledger", ledger, "--apply")
	if r.code != 0 {
		t.Fatalf("retain --apply exited %d\n%s", r.code, r.all())
	}
	after := fileState(t, lib)

	// Nothing was destroyed: the exact same set of file contents still exists.
	if b, a := contentMultiset(before), contentMultiset(after); len(b) != len(a) {
		t.Fatalf("file count changed: %d -> %d (a move must not unlink)", len(b), len(a))
	} else {
		for i := range b {
			if b[i] != a[i] {
				t.Errorf("content set changed at %d: %s -> %s", i, b[i], a[i])
			}
		}
	}
	// The two old clips moved into the archive; the fresh one did not.
	for _, want := range []string{"_archive/ancient.jsonl", "_archive/q1/older.jsonl", "fresh.jsonl"} {
		if _, ok := after[want]; !ok {
			t.Errorf("expected %s to exist after --apply; have %v", want, keys(after))
		}
	}
	for _, gone := range []string{"ancient.jsonl", "q1/older.jsonl"} {
		if _, ok := after[gone]; ok {
			t.Errorf("%s should have moved out of the library root", gone)
		}
	}
	// Archived content is byte-identical to what was there before.
	if after["_archive/ancient.jsonl"] != before["ancient.jsonl"] {
		t.Error("archived copy of ancient.jsonl differs from the original")
	}
	if after["_archive/q1/older.jsonl"] != before["q1/older.jsonl"] {
		t.Error("archived copy of q1/older.jsonl differs from the original")
	}
	if after["fresh.jsonl"] != before["fresh.jsonl"] {
		t.Error("the retained clip was altered")
	}

	rec := lastLedger(t, ledger)
	if rec["applied"] != true {
		t.Errorf("ledger applied=%v, want true", rec["applied"])
	}
	if rec["clips_moved"].(float64) != 2 {
		t.Errorf("ledger clips_moved=%v, want 2", rec["clips_moved"])
	}
	if rec["errors"].(float64) != 0 {
		t.Errorf("ledger errors=%v, want 0", rec["errors"])
	}

	// A second --apply is a no-op: the archive is skipped when walking.
	r2 := runCLI(t, "retain", "--lib", lib, "--older-than", "90d", "--ledger", ledger, "--apply")
	if r2.code != 0 {
		t.Fatalf("second retain --apply exited %d\n%s", r2.code, r2.all())
	}
	if final := fileState(t, lib); len(final) != len(after) {
		t.Errorf("second apply changed the library: %d -> %d files", len(after), len(final))
	}
}

func TestRetainRequiresLedger(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	setupRetentionLib(t, lib)
	before := fileState(t, lib)
	r := runCLI(t, "retain", "--lib", lib, "--older-than", "90d", "--apply")
	if r.code != 1 {
		t.Errorf("retain without --ledger should exit 1, got %d", r.code)
	}
	if len(fileState(t, lib)) != len(before) {
		t.Error("a rejected retain run still moved files")
	}
}

func lastLedger(t *testing.T, path string) map[string]any {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("ledger not written: %v", err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	var rec map[string]any
	if err := json.Unmarshal([]byte(lines[len(lines)-1]), &rec); err != nil {
		t.Fatalf("ledger line is not JSON: %v", err)
	}
	return rec
}

func keys(m map[string]string) []string {
	var out []string
	for k := range m {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

func TestParseAge(t *testing.T) {
	cases := []struct {
		in      string
		hours   float64
		wantErr bool
	}{
		{"90d", 90 * 24, false},
		{"12h", 12, false},
		{"6w", 6 * 7 * 24, false},
		{"1y", 365 * 24, false},
		{"30m", 0.5, false},
		{"45s", 45.0 / 3600, false},
		{"1.5d", 36, false},
		{"90 days", 90 * 24, false},
		{"90", 0, true}, // a bare number is ambiguous and rejected
		{"", 0, true},
		{"d", 0, true},
		{"90q", 0, true},
		{"-5d", 0, true},
	}
	for _, tc := range cases {
		got, err := parseAge(tc.in)
		if tc.wantErr {
			if err == nil {
				t.Errorf("parseAge(%q) should have failed, got %v", tc.in, got)
			}
			continue
		}
		if err != nil {
			t.Errorf("parseAge(%q) failed: %v", tc.in, err)
			continue
		}
		if diff := got.Hours() - tc.hours; diff > 1e-6 || diff < -1e-6 {
			t.Errorf("parseAge(%q) = %v hours, want %v", tc.in, got.Hours(), tc.hours)
		}
	}
}

// ---------------------------------------------------------------------------
// 7. Index and search correctness
// ---------------------------------------------------------------------------

func TestIndexCorrectness(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	writeCast(t, filepath.Join(lib, "beta.jsonl"), "second clip", []string{"sh", "-c", "echo beta"},
		"2026-05-05T05:05:05Z", [][3]string{
			{"0.100000", "o", "hello beta\n"},
			{"2.500000", "e", "warn\n"},
		})
	writeCast(t, filepath.Join(lib, "alpha.jsonl"), "first clip", []string{"sh", "-c", "echo alpha"},
		"2026-04-04T04:04:04Z", [][3]string{{"1.250000", "o", "hello alpha\n"}})
	writeCast(t, filepath.Join(lib, "sub", "gamma.cast"), "nested clip", []string{"sh"},
		"2026-06-06T06:06:06Z", [][3]string{{"0.500000", "o", "hello gamma\n"}})
	// Files that are not casts must be ignored entirely.
	if err := os.WriteFile(filepath.Join(lib, "notes.txt"), []byte("not a cast"), 0o644); err != nil {
		t.Fatal(err)
	}

	idxFile := filepath.Join(dir, "library.json")
	if r := runCLI(t, "index", "--lib", lib, "--index", idxFile); r.code != 0 {
		t.Fatalf("index exited %d\n%s", r.code, r.all())
	}
	idx, err := loadIndex(idxFile)
	if err != nil {
		t.Fatal(err)
	}
	if len(idx.Clips) != 3 {
		t.Fatalf("indexed %d clips, want 3 (%v)", len(idx.Clips), idx.Clips)
	}
	// Stable, sorted output.
	wantOrder := []string{"alpha.jsonl", "beta.jsonl", "sub/gamma.cast"}
	for i, w := range wantOrder {
		if idx.Clips[i].Path != w {
			t.Errorf("clip %d is %q, want %q (index must be sorted)", i, idx.Clips[i].Path, w)
		}
	}
	byPath := map[string]clipEntry{}
	for _, c := range idx.Clips {
		byPath[c.Path] = c
	}
	beta := byPath["beta.jsonl"]
	if beta.Title != "second clip" {
		t.Errorf("title = %q", beta.Title)
	}
	if beta.Events != 2 {
		t.Errorf("events = %d, want 2", beta.Events)
	}
	if beta.Duration != 2.5 {
		t.Errorf("duration = %v, want 2.5", beta.Duration)
	}
	if beta.StartedAt != "2026-05-05T05:05:05Z" {
		t.Errorf("started_at = %q", beta.StartedAt)
	}
	if beta.Width != 120 || beta.Height != 34 {
		t.Errorf("dimensions = %dx%d, want 120x34", beta.Width, beta.Height)
	}
	if beta.StdoutBytes != int64(len("hello beta\n")) {
		t.Errorf("stdout_bytes = %d", beta.StdoutBytes)
	}
	if beta.StderrBytes != int64(len("warn\n")) {
		t.Errorf("stderr_bytes = %d", beta.StderrBytes)
	}
	if want := fileSHA(t, filepath.Join(lib, "beta.jsonl")); beta.SHA256 != want {
		t.Errorf("sha256 = %s, want %s", beta.SHA256, want)
	}
	if !beta.Complete || beta.ExitCode == nil || *beta.ExitCode != 0 {
		t.Errorf("footer not parsed: complete=%v exit=%v", beta.Complete, beta.ExitCode)
	}

	// Two identical runs of index produce identical clip lists.
	idx2, err := buildIndex(lib)
	if err != nil {
		t.Fatal(err)
	}
	for i := range idx.Clips {
		if idx.Clips[i].Path != idx2.Clips[i].Path || idx.Clips[i].SHA256 != idx2.Clips[i].SHA256 {
			t.Error("index is not stable across runs")
		}
	}
}

func TestSearchMatchesTextContentNotJustFilenames(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	writeCast(t, filepath.Join(lib, "aaa.jsonl"), "unrelated title", []string{"sh"},
		"2026-05-05T05:05:05Z", [][3]string{
			{"0.100000", "o", "starting migration of the orders table\n"},
			{"4.250000", "o", "migration finished\n"},
		})
	writeCast(t, filepath.Join(lib, "bbb.jsonl"), "unrelated title", []string{"sh"},
		"2026-05-06T05:05:05Z", [][3]string{{"0.100000", "o", "nothing interesting here\n"}})

	var rep searchReport
	r := runCLI(t, "search", "migration", "--lib", lib, "--json")
	if r.code != 0 {
		t.Fatalf("search exited %d\n%s", r.code, r.all())
	}
	if err := json.Unmarshal([]byte(r.stdout), &rep); err != nil {
		t.Fatalf("search --json is not valid JSON: %v", err)
	}
	if len(rep.Results) != 1 {
		t.Fatalf("matched %d clips, want 1", len(rep.Results))
	}
	got := rep.Results[0]
	if got.Path != "aaa.jsonl" {
		t.Errorf("matched %q, want aaa.jsonl", got.Path)
	}
	if got.TextMatches != 2 {
		t.Errorf("text_matches = %d, want 2", got.TextMatches)
	}
	if got.NameMatch || got.TitleMatch {
		t.Error("match should have come from the transcript, not the filename or title")
	}
	if len(got.Hits) != 2 {
		t.Fatalf("hits = %d, want 2", len(got.Hits))
	}
	if got.Hits[0].Time != 0.1 || got.Hits[1].Time != 4.25 {
		t.Errorf("hit timestamps = %v, %v; want 0.1, 4.25", got.Hits[0].Time, got.Hits[1].Time)
	}
	if !strings.Contains(got.Hits[1].Excerpt, "migration finished") {
		t.Errorf("excerpt = %q", got.Hits[1].Excerpt)
	}

	// Filename-only and title-only matches still work.
	r2 := runCLI(t, "search", "bbb", "--lib", lib, "--json")
	var rep2 searchReport
	if err := json.Unmarshal([]byte(r2.stdout), &rep2); err != nil {
		t.Fatal(err)
	}
	if len(rep2.Results) != 1 || !rep2.Results[0].NameMatch {
		t.Errorf("filename search failed: %+v", rep2.Results)
	}

	// A term that matches nothing matches nothing.
	r3 := runCLI(t, "search", "zzzz-no-such-term", "--lib", lib, "--json")
	var rep3 searchReport
	if err := json.Unmarshal([]byte(r3.stdout), &rep3); err != nil {
		t.Fatal(err)
	}
	if len(rep3.Results) != 0 {
		t.Errorf("expected no matches, got %d", len(rep3.Results))
	}
}

func TestSearchCannotRecoverASecret(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	leakyCast(t, filepath.Join(lib, "deploy.jsonl"))
	for _, s := range allSecrets {
		r := runCLI(t, "search", s, "--lib", lib, "--json")
		var rep searchReport
		if err := json.Unmarshal([]byte(r.stdout), &rep); err != nil {
			t.Fatalf("search --json invalid: %v", err)
		}
		if rep.TotalMatches != 0 {
			t.Errorf("searching for %s... found %d transcript matches; the transcript must be redacted first",
				s[:6], rep.TotalMatches)
		}
	}
}

// ---------------------------------------------------------------------------
// 8. CLI contract
// ---------------------------------------------------------------------------

func TestHelpGoesToStdoutAndExitsZero(t *testing.T) {
	for _, args := range [][]string{{"-h"}, {"--help"}, {"help"}, {"scan", "--help"}, {"retain", "-h"}} {
		r := runCLI(t, args...)
		if r.code != 0 {
			t.Errorf("%v exited %d, want 0", args, r.code)
		}
		if !strings.Contains(r.stdout, "USAGE") {
			t.Errorf("%v did not print usage to stdout", args)
		}
		if r.stderr != "" {
			t.Errorf("%v wrote to stderr: %q", args, r.stderr)
		}
	}
}

func TestBadInvocationGoesToStderrAndExitsOne(t *testing.T) {
	cases := [][]string{
		{},
		{"nonsense"},
		{"scan"},
		{"index"},
		{"list"},
		{"search"},
		{"redact"},
		{"retain"},
		{"retain", "--lib", "."},
		{"retain", "--lib", ".", "--older-than", "banana", "--ledger", "x.jsonl"},
	}
	for _, args := range cases {
		r := runCLI(t, args...)
		if r.code != 1 {
			t.Errorf("%v exited %d, want 1", args, r.code)
		}
		if r.stdout != "" {
			t.Errorf("%v wrote to stdout: %q", args, r.stdout)
		}
		if !strings.Contains(r.stderr, "USAGE") && !strings.Contains(r.stderr, appName+":") {
			t.Errorf("%v did not print a diagnostic to stderr: %q", args, r.stderr)
		}
	}
}

func TestFlagsMayFollowPositionalArguments(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	writeCast(t, filepath.Join(lib, "a.jsonl"), "x", []string{"sh"},
		"2026-05-05T05:05:05Z", [][3]string{{"0.100000", "o", "needle here\n"}})

	// term first, then flags - the reorderFlags() contract
	r1 := runCLI(t, "search", "needle", "--lib", lib, "--json")
	// flags first, then term
	r2 := runCLI(t, "search", "--lib", lib, "--json", "needle")
	if r1.code != 0 || r2.code != 0 {
		t.Fatalf("exit codes %d / %d\n%s\n%s", r1.code, r2.code, r1.all(), r2.all())
	}
	var a, b searchReport
	if err := json.Unmarshal([]byte(r1.stdout), &a); err != nil {
		t.Fatal(err)
	}
	if err := json.Unmarshal([]byte(r2.stdout), &b); err != nil {
		t.Fatal(err)
	}
	if a.TotalMatches != 1 || b.TotalMatches != 1 {
		t.Errorf("matches %d vs %d, want 1 each", a.TotalMatches, b.TotalMatches)
	}

	// Same for a value flag after a positional on scan.
	r3 := runCLI(t, "scan", filepath.Join(lib, "a.jsonl"), "--json")
	if r3.code != 0 {
		t.Errorf("scan <file> --json exited %d\n%s", r3.code, r3.all())
	}
}

func TestReorderFlags(t *testing.T) {
	vf := map[string]bool{"lib": true, "out": true}
	cases := []struct {
		in, want []string
	}{
		{[]string{"term", "--lib", "d"}, []string{"--lib", "d", "term"}},
		{[]string{"--lib", "d", "term"}, []string{"--lib", "d", "term"}},
		{[]string{"a", "--json", "b"}, []string{"--json", "a", "b"}},
		{[]string{"x", "--out", "o", "--json"}, []string{"--out", "o", "--json", "x"}},
		{nil, nil},
	}
	for _, tc := range cases {
		got := reorderFlags(tc.in, vf)
		if strings.Join(got, "\x00") != strings.Join(tc.want, "\x00") {
			t.Errorf("reorderFlags(%v) = %v, want %v", tc.in, got, tc.want)
		}
	}
}

func TestHumanBytes(t *testing.T) {
	cases := []struct {
		in   int64
		want string
	}{
		{0, "0 B"},
		{512, "512 B"},
		{1023, "1023 B"},
		{1024, "1.0 KiB"},
		{1536, "1.5 KiB"},
		{1048576, "1.0 MiB"},
		{1073741824, "1.0 GiB"},
	}
	for _, tc := range cases {
		if got := humanBytes(tc.in); got != tc.want {
			t.Errorf("humanBytes(%d) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

func TestJSONOnEveryReportingSubcommand(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	leakyCast(t, filepath.Join(lib, "deploy.jsonl"))
	idxFile := filepath.Join(dir, "library.json")
	runCLI(t, "index", "--lib", lib, "--index", idxFile)

	cases := [][]string{
		{"index", "--lib", lib, "--json"},
		{"list", "--index", idxFile, "--json"},
		{"search", "deploy", "--index", idxFile, "--json"},
		{"scan", "--lib", lib, "--json"},
		{"redact", "--lib", lib, "--out-dir", filepath.Join(dir, "share"), "--json"},
		{"retain", "--lib", lib, "--older-than", "1d", "--ledger", filepath.Join(dir, "l.jsonl"), "--json"},
	}
	for _, args := range cases {
		r := runCLI(t, args...)
		if r.code != 0 {
			t.Errorf("%v exited %d\n%s", args, r.code, r.all())
			continue
		}
		var v any
		if err := json.Unmarshal([]byte(r.stdout), &v); err != nil {
			t.Errorf("%v did not emit valid JSON: %v\n%s", args, err, r.stdout)
		}
	}
}

func TestMalformedCastIsReportedWithoutEchoingContent(t *testing.T) {
	dir := t.TempDir()
	lib := filepath.Join(dir, "clips")
	if err := os.MkdirAll(lib, 0o755); err != nil {
		t.Fatal(err)
	}
	bad := filepath.Join(lib, "broken.jsonl")
	// A valid header, then a line that is not a valid event but does contain a
	// credential. The error message must not quote it.
	content := `{"version":1,"command":["sh"],"started_at":"2026-01-01T00:00:00Z","shell":"/bin/sh","width":80,"height":24,"title":"t"}` + "\n" +
		`[not-json, "o", "PASSWORD=` + secAssigned + `"]` + "\n"
	if err := os.WriteFile(bad, []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
	r := runCLI(t, "scan", "--lib", lib)
	if strings.Contains(r.all(), secAssigned) {
		t.Error("a parse error echoed file content containing a credential")
	}
	if !strings.Contains(r.all(), "line 2") {
		t.Errorf("parse error should name the offending line:\n%s", r.all())
	}
}

func fileSHA(t *testing.T, path string) string {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])
}
