package app

import (
	"Picocrypt-NG/internal/encoding"
	"Picocrypt-NG/internal/util"
	"errors"
	"image/color"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
)

// repoRoot walks up from the test working directory to the repository root —
// the first ancestor that contains both the VERSION file and .github/workflows.
// Mirrors the established pattern in internal/distmeta and internal/workflowpolicy
// (no repoRoot exists in package app). Used by TestStateVersion to tie the
// app.Version const to the canonical VERSION file.
func repoRoot(t *testing.T) string {
	t.Helper()
	wd, err := os.Getwd()
	if err != nil {
		t.Fatalf("getwd: %v", err)
	}
	current := wd
	for {
		_, verErr := os.Stat(filepath.Join(current, "VERSION"))
		_, wfErr := os.Stat(filepath.Join(current, ".github", "workflows"))
		if verErr == nil && wfErr == nil {
			return current
		}
		parent := filepath.Dir(current)
		if parent == current {
			t.Fatal("could not find repository root (dir with VERSION and .github/workflows) from test working directory")
		}
		current = parent
	}
}

// mustNewState builds a *State for tests, failing the test if RS-codec
// initialization returns an error. Centralizes the (*State, error) call so the
// many table tests below stay readable after APP-01 changed NewState's signature.
func mustNewState(t *testing.T) *State {
	t.Helper()
	s, err := NewState()
	if err != nil {
		t.Fatalf("NewState() returned error: %v", err)
	}
	return s
}

// TestNewStateRSInitFailure proves APP-01/D-05: when the RS-codec constructor
// fails, NewState returns a non-nil error (wrapping the cause) and a nil *State,
// and never panics. The newRSCodecs package-level seam is overridden to force
// the failure, mirroring the Phase 3/4 var-seam override+restore pattern.
func TestNewStateRSInitFailure(t *testing.T) {
	orig := newRSCodecs
	t.Cleanup(func() { newRSCodecs = orig })

	forced := errors.New("forced RS init failure")
	newRSCodecs = func() (*encoding.RSCodecs, error) {
		return nil, forced
	}

	// Must not panic; recover only to surface a clearer failure if it does.
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("NewState() panicked instead of returning an error: %v", r)
		}
	}()

	state, err := NewState()
	if err == nil {
		t.Fatal("NewState() error = nil; want non-nil on RS-init failure")
	}
	if state != nil {
		t.Errorf("NewState() state = %v; want nil on error", state)
	}
	if !errors.Is(err, forced) {
		t.Errorf("NewState() error = %v; want wrapping %v", err, forced)
	}
	if !strings.Contains(err.Error(), "init RS codecs") {
		t.Errorf("NewState() error = %q; want it to mention 'init RS codecs'", err.Error())
	}
}

func TestNewState(t *testing.T) {
	state := mustNewState(t)

	// Check defaults
	if state.InputLabel != "Drop files and folders into this window" {
		t.Errorf("InputLabel = %q; want default", state.InputLabel)
	}
	if state.KeyfileLabel != "None selected" {
		t.Errorf("KeyfileLabel = %q; want 'None selected'", state.KeyfileLabel)
	}
	if state.CommentsLabel != "Comments:" {
		t.Errorf("CommentsLabel = %q; want 'Comments:'", state.CommentsLabel)
	}
	if state.StartLabel != "Start" {
		t.Errorf("StartLabel = %q; want 'Start'", state.StartLabel)
	}
	if state.MainStatus != "Ready" {
		t.Errorf("MainStatus = %q; want 'Ready'", state.MainStatus)
	}
	if state.MainStatusColor != util.WHITE {
		t.Error("MainStatusColor should be WHITE")
	}
	if state.PasswordMode != PasswordModeHidden {
		t.Error("PasswordMode should be Hidden")
	}
	if state.PasswordStateLabel != "Show" {
		t.Errorf("PasswordStateLabel = %q; want 'Show'", state.PasswordStateLabel)
	}
	if state.PassgenLength != 32 {
		t.Errorf("PassgenLength = %d; want 32", state.PassgenLength)
	}
	if state.SplitSelected != 1 {
		t.Errorf("SplitSelected = %d; want 1 (MiB)", state.SplitSelected)
	}
	if !state.FastDecode {
		t.Error("FastDecode should be true by default")
	}
	if state.DPI != 1.0 {
		t.Errorf("DPI = %f; want 1.0", state.DPI)
	}

	// Check RS codecs are initialized
	if state.RSCodecs == nil {
		t.Error("RSCodecs should be initialized")
	}
	if state.RS1 == nil || state.RS128 == nil {
		t.Error("RS codecs should be initialized")
	}
}

func TestStateReset(t *testing.T) {
	state := mustNewState(t)

	// Modify state
	state.Mode = "encrypt"
	state.Working = true
	state.Password = "secret"
	state.Keyfiles = []string{"key1", "key2"}
	state.Paranoid = true
	state.ShowProgress = true

	// Reset
	state.Reset()

	// Check all fields are reset
	if state.Mode != "" {
		t.Errorf("Mode = %q; want empty", state.Mode)
	}
	if state.Working {
		t.Error("Working should be false")
	}
	if state.Password != "" {
		t.Error("Password should be empty")
	}
	if len(state.Keyfiles) != 0 {
		t.Error("Keyfiles should be empty")
	}
	if state.Paranoid {
		t.Error("Paranoid should be false")
	}
	if state.ShowProgress {
		t.Error("ShowProgress should be false")
	}

	// Check defaults are restored
	if state.MainStatus != "Ready" {
		t.Errorf("MainStatus = %q; want 'Ready'", state.MainStatus)
	}
}

func TestStateResetUI(t *testing.T) {
	state := mustNewState(t)

	// Set progress-related flags that ResetUI must PRESERVE (the delta vs Reset).
	state.Working = true
	state.ShowProgress = true
	state.CanCancel = true

	// Set other state that ResetUI must clear.
	state.Mode = "encrypt"
	state.Password = "secret"

	// ResetUI preserves the progress flags; Reset clears them.
	// Swapping this call to state.Reset() must turn the assertions below RED.
	state.ResetUI()

	// Progress-related flags: ResetUI must NOT touch these.
	if !state.Working {
		t.Error("Working should be preserved by ResetUI (Reset clears it; ResetUI does not)")
	}
	if !state.ShowProgress {
		t.Error("ShowProgress should be preserved by ResetUI (Reset clears it; ResetUI does not)")
	}
	if !state.CanCancel {
		t.Error("CanCancel should be preserved by ResetUI (Reset clears it; ResetUI does not)")
	}

	// Other state should be reset by both Reset and ResetUI.
	if state.Mode != "" {
		t.Errorf("Mode = %q; want empty", state.Mode)
	}
	if state.Password != "" {
		t.Error("Password should be empty")
	}
}

// TestStateResetUIVsReset asserts the exact behavioral delta between ResetUI and
// Reset: Reset clears Working, ShowProgress, CanCancel, and Scanning; ResetUI
// preserves all four. A test that passes whether we call Reset or ResetUI is not
// a real test (Rule 9). This test pins the delta so swapping Reset↔ResetUI
// makes it FAIL. ModalID is preserved by both; it is not part of the delta.
func TestStateResetUIVsReset(t *testing.T) {
	// Verify Reset DOES clear the progress flags (control group).
	t.Run("Reset clears progress flags", func(t *testing.T) {
		s := mustNewState(t)
		s.Working = true
		s.ShowProgress = true
		s.CanCancel = true
		s.Scanning = true

		s.Reset()

		if s.Working {
			t.Error("Reset: Working should be false after Reset")
		}
		if s.ShowProgress {
			t.Error("Reset: ShowProgress should be false after Reset")
		}
		if s.CanCancel {
			t.Error("Reset: CanCancel should be false after Reset")
		}
		if s.Scanning {
			t.Error("Reset: Scanning should be false after Reset")
		}
	})

	// Verify ResetUI does NOT clear the same flags.
	t.Run("ResetUI preserves progress flags", func(t *testing.T) {
		s := mustNewState(t)
		s.Working = true
		s.ShowProgress = true
		s.CanCancel = true
		s.Scanning = true

		s.ResetUI()

		if !s.Working {
			t.Error("ResetUI: Working should remain true (only Reset clears it)")
		}
		if !s.ShowProgress {
			t.Error("ResetUI: ShowProgress should remain true (only Reset clears it)")
		}
		if !s.CanCancel {
			t.Error("ResetUI: CanCancel should remain true (only Reset clears it)")
		}
		if !s.Scanning {
			t.Error("ResetUI: Scanning should remain true (only Reset clears it)")
		}
	})
}

func TestStateResetAfterOperation(t *testing.T) {
	state := mustNewState(t)

	state.Working = true
	state.ShowProgress = true
	state.CanCancel = true
	state.Progress = 0.75
	state.ProgressInfo = "75%"

	state.ResetAfterOperation()

	if state.Working {
		t.Error("Working should be false")
	}
	if state.ShowProgress {
		t.Error("ShowProgress should be false")
	}
	if state.CanCancel {
		t.Error("CanCancel should be false")
	}
	if state.Progress != 0 {
		t.Errorf("Progress = %f; want 0", state.Progress)
	}
	if state.ProgressInfo != "" {
		t.Errorf("ProgressInfo = %q; want empty", state.ProgressInfo)
	}
}

func TestIsEncryptingDecrypting(t *testing.T) {
	state := mustNewState(t)

	// Initially neither
	if state.IsEncrypting() {
		t.Error("Should not be encrypting initially")
	}
	if state.IsDecrypting() {
		t.Error("Should not be decrypting initially")
	}

	// Set encrypt mode
	state.Mode = "encrypt"
	if !state.IsEncrypting() {
		t.Error("Should be encrypting")
	}
	if state.IsDecrypting() {
		t.Error("Should not be decrypting in encrypt mode")
	}

	// Set decrypt mode
	state.Mode = "decrypt"
	if state.IsEncrypting() {
		t.Error("Should not be encrypting in decrypt mode")
	}
	if !state.IsDecrypting() {
		t.Error("Should be decrypting")
	}
}

func TestCanStart(t *testing.T) {
	state := mustNewState(t)

	// No credentials
	if state.CanStart() {
		t.Error("Should not be able to start without credentials")
	}

	// With password only
	state.Password = "secret"
	if !state.CanStart() {
		t.Error("Should be able to start with password")
	}

	// With keyfiles only
	state.Password = ""
	state.Keyfiles = []string{"keyfile.bin"}
	if !state.CanStart() {
		t.Error("Should be able to start with keyfiles")
	}

	// Encrypt mode with mismatched passwords
	state.Mode = "encrypt"
	state.Password = "secret"
	state.CPassword = "different"
	if state.CanStart() {
		t.Error("Should not be able to start with mismatched passwords")
	}

	// Encrypt mode with matching passwords
	state.CPassword = "secret"
	if !state.CanStart() {
		t.Error("Should be able to start with matching passwords")
	}

	// Decrypt mode ignores CPassword
	state.Mode = "decrypt"
	state.CPassword = "different"
	if !state.CanStart() {
		t.Error("Decrypt mode should not check password confirmation")
	}
}

func TestTogglePasswordVisibility(t *testing.T) {
	state := mustNewState(t)

	// Initially hidden
	if state.PasswordMode != PasswordModeHidden {
		t.Error("Should start hidden")
	}
	if state.PasswordStateLabel != "Show" {
		t.Error("Label should be 'Show'")
	}

	// Toggle to visible
	state.TogglePasswordVisibility()
	if state.PasswordMode != PasswordModeVisible {
		t.Error("Should be visible after toggle")
	}
	if state.PasswordStateLabel != "Hide" {
		t.Error("Label should be 'Hide'")
	}

	// Toggle back to hidden
	state.TogglePasswordVisibility()
	if state.PasswordMode != PasswordModeHidden {
		t.Error("Should be hidden after second toggle")
	}
	if state.PasswordStateLabel != "Show" {
		t.Error("Label should be 'Show'")
	}
}

func TestIsPasswordHidden(t *testing.T) {
	state := mustNewState(t)

	if !state.IsPasswordHidden() {
		t.Error("Password should be hidden initially")
	}

	state.PasswordMode = PasswordModeVisible
	if state.IsPasswordHidden() {
		t.Error("Password should not be hidden when visible")
	}
}

func TestUpdateKeyfileLabel(t *testing.T) {
	state := mustNewState(t)

	// No keyfiles, not required
	state.Keyfile = false
	state.Keyfiles = nil
	state.UpdateKeyfileLabel()
	if state.KeyfileLabel != "None selected" {
		t.Errorf("KeyfileLabel = %q; want 'None selected'", state.KeyfileLabel)
	}

	// No keyfiles, but required
	state.Keyfile = true
	state.UpdateKeyfileLabel()
	if state.KeyfileLabel != "Keyfiles required" {
		t.Errorf("KeyfileLabel = %q; want 'Keyfiles required'", state.KeyfileLabel)
	}

	// One keyfile
	state.Keyfiles = []string{"key1.bin"}
	state.UpdateKeyfileLabel()
	if state.KeyfileLabel != "Using 1 keyfile" {
		t.Errorf("KeyfileLabel = %q; want 'Using 1 keyfile'", state.KeyfileLabel)
	}

	// Multiple keyfiles
	state.Keyfiles = []string{"key1.bin", "key2.bin", "key3.bin"}
	state.UpdateKeyfileLabel()
	if state.KeyfileLabel != "Using multiple keyfiles" {
		t.Errorf("KeyfileLabel = %q; want 'Using multiple keyfiles'", state.KeyfileLabel)
	}
}

// TestSetStatus pins the binding between SetStatus's two args and the two fields it
// writes. Two DISTINCT non-default rows ({text, color} pairs that differ in BOTH
// components) are required so the test fails if SetStatus ignores an arg, writes a
// constant, or swaps text↔color (a single row that happened to match a default could
// not catch a no-op). Both fields are asserted independently per row.
func TestSetStatus(t *testing.T) {
	tests := []struct {
		name  string
		text  string
		color color.RGBA
	}{
		{name: "working/red", text: "Working…", color: util.RED},
		{name: "done/green", text: "Done", color: util.GREEN},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			state := mustNewState(t)
			state.SetStatus(tt.text, tt.color)

			if state.MainStatus != tt.text {
				t.Errorf("MainStatus = %q; want %q", state.MainStatus, tt.text)
			}
			if state.MainStatusColor != tt.color {
				t.Errorf("MainStatusColor = %v; want %v", state.MainStatusColor, tt.color)
			}
		})
	}
}

// TestSetPopupStatus pins that SetPopupStatus writes its arg AND that a later call
// overwrites an earlier value. The second sequential write ("B" after "A") is the
// load-bearing assertion: a set-once mutation (writing only when empty) or one that
// ignores its arg would keep "A" and fail here, where a single write could not tell
// "stored the arg" from "stored a constant that happened to match".
func TestSetPopupStatus(t *testing.T) {
	state := mustNewState(t)

	state.SetPopupStatus("A")
	if state.PopupStatus != "A" {
		t.Errorf("PopupStatus = %q; want %q", state.PopupStatus, "A")
	}

	state.SetPopupStatus("B")
	if state.PopupStatus != "B" {
		t.Errorf("PopupStatus = %q after second write; want %q (the setter must overwrite, not set-once)", state.PopupStatus, "B")
	}
}

// TestSetProgress pins both args of SetProgress to their fields. Two rows with
// distinct fraction AND info (0.0/"0%" and 1.0/"100%") catch an arg-swap (fraction
// vs info are different types so a swap won't compile, but a constant fraction or a
// dropped info would slip past a single row), a constant return, or a dropped write.
func TestSetProgress(t *testing.T) {
	tests := []struct {
		name     string
		fraction float32
		info     string
	}{
		{name: "zero", fraction: 0.0, info: "0%"},
		{name: "full", fraction: 1.0, info: "100%"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			state := mustNewState(t)
			state.SetProgress(tt.fraction, tt.info)

			if state.Progress != tt.fraction {
				t.Errorf("Progress = %f; want %f", state.Progress, tt.fraction)
			}
			if state.ProgressInfo != tt.info {
				t.Errorf("ProgressInfo = %q; want %q", state.ProgressInfo, tt.info)
			}
		})
	}
}

func TestSetCanCancel(t *testing.T) {
	state := mustNewState(t)

	state.SetCanCancel(true)
	if !state.CanCancel {
		t.Error("CanCancel should be true")
	}

	state.SetCanCancel(false)
	if state.CanCancel {
		t.Error("CanCancel should be false")
	}
}

func TestGenPassword(t *testing.T) {
	state := mustNewState(t)

	// Set passgen options
	state.PassgenLength = 20
	state.PassgenUpper = true
	state.PassgenLower = true
	state.PassgenNums = true
	state.PassgenSymbols = false
	state.PassgenCopy = false

	password := state.GenPassword()

	if len(password) != 20 {
		t.Errorf("Password length = %d; want 20", len(password))
	}

	// Should not contain symbols
	for _, ch := range password {
		if ch == '-' || ch == '=' || ch == '_' || ch == '+' || ch == '!' {
			t.Error("Password should not contain symbols")
		}
	}
}

func TestGenPasswordNoOptions(t *testing.T) {
	state := mustNewState(t)

	// No character sets enabled
	state.PassgenLength = 10
	state.PassgenUpper = false
	state.PassgenLower = false
	state.PassgenNums = false
	state.PassgenSymbols = false
	state.PassgenCopy = false

	password := state.GenPassword()

	if password != "" {
		t.Errorf("Password = %q; want empty when no char sets", password)
	}
}

// TestStateWorkerCallbackConcurrency models the APP-02 worker↔render boundary:
// one set of goroutines plays the encrypt/decrypt worker (reading the request
// fields via Snapshot() and writing status/result via the locked setters), while
// another set plays the Fyne render-thread callbacks (reading/writing the same
// fields via the locked getters/setters). It must be clean under `go test -race`
// — it is the project-code race the APP-02 gate keeps green (D-06).
//
// This test will not compile until State grows Snapshot() plus the missing
// locked accessors (Mode/Working/InputFile/OutputFile/Comments/Deniability/
// Keep/Kept). That non-compiling state is the RED phase.
func TestStateWorkerCallbackConcurrency(t *testing.T) {
	state, err := NewState()
	if err != nil {
		t.Fatalf("NewState() error: %v", err)
	}

	const iterations = 200
	var wg sync.WaitGroup

	// Worker goroutine: build a request snapshot under one RLock, then write
	// status/result through the locked setters — exactly the operations.go
	// doEncrypt/doDecrypt hot-path access pattern.
	wg.Go(func() {
		for i := range iterations {
			snap := state.Snapshot()
			// Touch the snapshot fields so the race detector sees the reads.
			_ = snap.Mode
			_ = snap.InputFile
			_ = snap.OutputFile
			_ = snap.Password
			_ = snap.Keyfiles
			_ = snap.Comments
			_ = snap.Paranoid
			_ = snap.ReedSolomon
			_ = snap.Deniability
			_ = snap.Compress
			_ = snap.VerifyFirst
			_ = snap.Keep
			state.SetStatus("working", util.WHITE)
			state.SetKept(i%2 == 0)
		}
	})

	// Render-thread callback goroutine: drives the locked setters the drop /
	// operations callbacks use to mutate shared fields concurrently.
	wg.Go(func() {
		for i := range iterations {
			state.SetMode("encrypt")
			state.SetWorking(i%2 == 0)
			state.SetInputFile("in.txt")
			state.SetOutputFile("out.pcv")
			state.SetComments("hello")
			state.SetDeniability(i%2 == 0)
			state.SetKeep(i%2 == 1)
		}
	})

	// Render-thread reader goroutine: hits the locked getters concurrently.
	wg.Go(func() {
		for range iterations {
			_ = state.IsEncrypting()
			_ = state.IsDecrypting()
			_ = state.IsWorking()
			rsnap := state.Snapshot()
			_ = rsnap.InputFile
			_ = rsnap.OutputFile
			_ = rsnap.Comments
			_ = rsnap.Deniability
			_ = rsnap.Keep
			_ = state.WasKept()
		}
	})

	wg.Wait()

	// Sanity: the accessors round-trip a written value consistently.
	state.SetMode("decrypt")
	if !state.IsDecrypting() {
		t.Fatal("IsDecrypting() = false after SetMode(\"decrypt\")")
	}
	state.SetWorking(true)
	if !state.IsWorking() {
		t.Fatal("IsWorking() = false after SetWorking(true)")
	}
}

func TestStateConcurrency(t *testing.T) {
	state := mustNewState(t)

	var wg sync.WaitGroup
	iterations := 100

	// Concurrent reads
	wg.Add(iterations)
	for range iterations {
		go func() {
			defer wg.Done()
			_ = state.IsEncrypting()
			_ = state.IsDecrypting()
			_ = state.CanStart()
			_ = state.IsPasswordHidden()
		}()
	}

	// Concurrent writes
	wg.Add(iterations)
	for i := range iterations {
		go func(i int) {
			defer wg.Done()
			state.SetStatus("Test", util.WHITE)
			state.SetPopupStatus("Status")
			state.SetProgress(float32(i)/float32(iterations), "")
			state.SetCanCancel(i%2 == 0)
			state.TogglePasswordVisibility()
			state.UpdateKeyfileLabel()
		}(i)
	}

	wg.Wait()
	t.Log("Concurrent access completed without deadlock")
}

// TestPasswordInputModeConstants verifies the named PasswordInputMode constants
// are wired correctly into their consumers, rather than asserting the trivial
// iota distinctness (0 != 1). It pins two bindings: NewState initializes
// PasswordMode to PasswordModeHidden, and IsPasswordHidden tracks the named
// constant at each value. This catches a consumer that hard-codes a literal or
// returns a constant answer.
//
// Residual overlap with TestIsPasswordHidden is minimal and intentional: that
// test exercises IsPasswordHidden in isolation, while this one ties the named
// constants to both the constructor default and the predicate together — the
// binding the original tautology failed to assert.
func TestPasswordInputModeConstants(t *testing.T) {
	state := mustNewState(t)

	// Constructor default is bound to the named constant, not a bare literal.
	if state.PasswordMode != PasswordModeHidden {
		t.Errorf("NewState PasswordMode = %d; want PasswordModeHidden (%d)", state.PasswordMode, PasswordModeHidden)
	}
	if !state.IsPasswordHidden() {
		t.Error("IsPasswordHidden() = false at PasswordModeHidden; want true")
	}

	// One toggle flips to the visible constant; the predicate must follow it.
	state.TogglePasswordVisibility()
	if state.PasswordMode != PasswordModeVisible {
		t.Errorf("after toggle PasswordMode = %d; want PasswordModeVisible (%d)", state.PasswordMode, PasswordModeVisible)
	}
	if state.IsPasswordHidden() {
		t.Error("IsPasswordHidden() = true at PasswordModeVisible; want false")
	}
}

// TestPassgenCopyDefaultsOff asserts PassgenCopy defaults to false both on
// construction and after Reset, so generated passwords are NOT auto-copied to
// the OS clipboard (clipboard sync / history is a strong-secret leak). The
// Copy buttons remain for explicit user action; only the default is changed.
func TestPassgenCopyDefaultsOff(t *testing.T) {
	t.Run("NewState", func(t *testing.T) {
		s := mustNewState(t)
		if s.PassgenCopy {
			t.Fatal("PassgenCopy must default to false — do not auto-copy generated passwords to the clipboard")
		}
	})
	t.Run("after Reset", func(t *testing.T) {
		s := mustNewState(t)
		s.PassgenCopy = true // simulate user enabling it
		s.Reset()
		if s.PassgenCopy {
			t.Fatal("PassgenCopy must be false after Reset — Reset must not restore the old auto-copy default")
		}
	})
}

// TestPassgenCharClassesDefaultOn asserts the four password-generator character
// classes default to ON, and — crucially — that NewState() agrees with Reset().
// resetUILocked() sets all four true, but a freshly-constructed State (before any
// ResetUI) must too: otherwise the generator dialog opens with every class
// unchecked and Generate is a no-op (dialogs.go guards against zero classes).
// This is a desync tripwire between the construction literal and the reset path,
// mirroring TestPassgenCopyDefaultsOff for the opposite-default sibling field.
func TestPassgenCharClassesDefaultOn(t *testing.T) {
	classes := func(s *State) [4]bool {
		return [4]bool{s.PassgenUpper, s.PassgenLower, s.PassgenNums, s.PassgenSymbols}
	}
	want := [4]bool{true, true, true, true}

	t.Run("NewState", func(t *testing.T) {
		if got := classes(mustNewState(t)); got != want {
			t.Fatalf("NewState passgen classes = %v; want %v — a fresh State must have all classes ON so the generator works before any reset", got, want)
		}
	})
	t.Run("agrees with Reset", func(t *testing.T) {
		s := mustNewState(t)
		s.PassgenUpper, s.PassgenLower, s.PassgenNums, s.PassgenSymbols = false, false, false, false
		s.Reset()
		if got := classes(s); got != want {
			t.Fatalf("after Reset passgen classes = %v; want %v", got, want)
		}
	})
}

// TestStateVersion is a desync tripwire, not a tautology: it asserts the
// app.Version const stays in lockstep with the canonical root VERSION file
// (app.Version must be "v" + <VERSION>). A version bump that edits VERSION but
// forgets state.go (or vice versa) fails here. This is non-duplicative of
// distmeta's TestActiveReleaseMetadataVersions, which validates VERSION against
// distribution metadata but never references app.Version.
func TestStateVersion(t *testing.T) {
	raw, err := os.ReadFile(filepath.Join(repoRoot(t), "VERSION"))
	if err != nil {
		t.Fatalf("read VERSION: %v", err)
	}
	want := "v" + strings.TrimSpace(string(raw))
	if Version != want {
		t.Fatalf("app.Version = %q; want %q (derived from root VERSION file)", Version, want)
	}
}
