package pipeline

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/mvanhorn/cli-printing-press/v4/internal/artifacts"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func setupLockTest(t *testing.T) (cleanup func()) {
	t.Helper()
	tmpDir := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmpDir)
	return func() {}
}

func TestAcquireLock_NoExistingLock(t *testing.T) {
	setupLockTest(t)

	lock, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)
	assert.Equal(t, "scope-1", lock.Scope)
	assert.Equal(t, "acquire", lock.Phase)
	assert.NotZero(t, lock.PID)
	assert.WithinDuration(t, time.Now(), lock.AcquiredAt, 2*time.Second)
	assert.WithinDuration(t, time.Now(), lock.UpdatedAt, 2*time.Second)

	// Verify the lock file exists and is valid JSON.
	data, err := os.ReadFile(LockFilePath("test-pp-cli"))
	require.NoError(t, err)
	var readBack LockState
	require.NoError(t, json.Unmarshal(data, &readBack))
	assert.Equal(t, "scope-1", readBack.Scope)
}

func TestAcquireLock_LocksDirectoryCreated(t *testing.T) {
	setupLockTest(t)

	_, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)

	info, err := os.Stat(LocksDir())
	require.NoError(t, err)
	assert.True(t, info.IsDir())
}

func TestAcquireLock_RebuildCase(t *testing.T) {
	setupLockTest(t)

	// Create a library directory to simulate rebuild scenario.
	libDir := filepath.Join(PublishedLibraryRoot(), "test-pp-cli")
	require.NoError(t, os.MkdirAll(libDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(libDir, "go.mod"), []byte("module test"), 0o644))

	lock, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)
	assert.Equal(t, "scope-1", lock.Scope)
}

func TestAcquireLock_StaleLockAutoReclaim(t *testing.T) {
	setupLockTest(t)

	// Create a stale lock.
	require.NoError(t, os.MkdirAll(LocksDir(), 0o755))
	staleLock := &LockState{
		Scope:      "old-scope",
		Phase:      "build",
		PID:        99999,
		AcquiredAt: time.Now().Add(-2 * time.Hour),
		UpdatedAt:  time.Now().Add(-2 * time.Hour),
	}
	require.NoError(t, writeLock(LockFilePath("test-pp-cli"), staleLock))

	lock, err := AcquireLock("test-pp-cli", "new-scope", false)
	require.NoError(t, err)
	assert.Equal(t, "new-scope", lock.Scope)
}

func TestAcquireLock_FreshLockDeadPIDAutoReclaim(t *testing.T) {
	setupLockTest(t)
	setLockOwnerAliveForTest(t, false)

	require.NoError(t, os.MkdirAll(LocksDir(), 0o755))
	deadLock := &LockState{
		Scope:      "old-scope",
		Phase:      "build",
		PID:        12345,
		AcquiredAt: time.Now(),
		UpdatedAt:  time.Now(),
	}
	require.NoError(t, writeLock(LockFilePath("test-pp-cli"), deadLock))

	lock, err := AcquireLock("test-pp-cli", "new-scope", false)
	require.NoError(t, err)
	assert.Equal(t, "new-scope", lock.Scope)
}

func TestAcquireLock_FreshLockDifferentScope_Blocked(t *testing.T) {
	setupLockTest(t)

	_, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)

	_, err = AcquireLock("test-pp-cli", "scope-2", false)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "lock held by scope")
}

func TestAcquireLock_FreshLockSameScope_Succeeds(t *testing.T) {
	setupLockTest(t)

	_, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)

	lock, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)
	assert.Equal(t, "scope-1", lock.Scope)
}

func TestAcquireLock_ForceOverridesFreshLock(t *testing.T) {
	setupLockTest(t)

	_, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)

	lock, err := AcquireLock("test-pp-cli", "scope-2", true)
	require.NoError(t, err)
	assert.Equal(t, "scope-2", lock.Scope)
}

func TestUpdateLock(t *testing.T) {
	setupLockTest(t)

	_, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)

	time.Sleep(10 * time.Millisecond) // Ensure time difference.

	err = UpdateLock("test-pp-cli", "build-p0")
	require.NoError(t, err)

	lock, err := readLock(LockFilePath("test-pp-cli"))
	require.NoError(t, err)
	assert.Equal(t, "build-p0", lock.Phase)
	assert.True(t, lock.UpdatedAt.After(lock.AcquiredAt))
}

func TestLockStatus_ActiveLock(t *testing.T) {
	setupLockTest(t)

	_, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)

	status := LockStatus("test-pp-cli")
	assert.True(t, status.Held)
	assert.False(t, status.Stale)
	assert.Equal(t, "acquire", status.Phase)
	assert.Equal(t, "scope-1", status.Scope)
	assert.NotNil(t, status.Lock)
}

func TestLockStatus_FreshLockDeadPIDIsStale(t *testing.T) {
	setupLockTest(t)
	setLockOwnerAliveForTest(t, false)

	require.NoError(t, os.MkdirAll(LocksDir(), 0o755))
	deadLock := &LockState{
		Scope:      "old-scope",
		Phase:      "build",
		PID:        12345,
		AcquiredAt: time.Now(),
		UpdatedAt:  time.Now(),
	}
	require.NoError(t, writeLock(LockFilePath("test-pp-cli"), deadLock))

	status := LockStatus("test-pp-cli")
	assert.True(t, status.Held)
	assert.True(t, status.Stale)
	assert.Equal(t, "build", status.Phase)
	assert.Equal(t, "old-scope", status.Scope)
}

func TestLockStatus_NoLock(t *testing.T) {
	setupLockTest(t)

	status := LockStatus("nonexistent-pp-cli")
	assert.False(t, status.Held)
	assert.False(t, status.HasCLI)
}

func TestLockStatus_NoLockWithLibraryCLI(t *testing.T) {
	setupLockTest(t)

	// Create library dir with go.mod (slug-keyed directory).
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	require.NoError(t, os.MkdirAll(libDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(libDir, "go.mod"), []byte("module test"), 0o644))

	status := LockStatus("test-pp-cli")
	assert.False(t, status.Held)
	assert.True(t, status.HasCLI)
}

func TestLockStatus_NoLockLibraryDirNoGoMod(t *testing.T) {
	setupLockTest(t)

	// Create library dir without go.mod (debris), slug-keyed.
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	require.NoError(t, os.MkdirAll(libDir, 0o755))

	status := LockStatus("test-pp-cli")
	assert.False(t, status.Held)
	assert.False(t, status.HasCLI)
}

func TestLockStatus_NoLockLibraryDirWithManifest(t *testing.T) {
	setupLockTest(t)

	// Create library dir with manifest but no go.mod (slug-keyed).
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	require.NoError(t, os.MkdirAll(libDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(libDir, CLIManifestFilename), []byte("{}"), 0o644))

	status := LockStatus("test-pp-cli")
	assert.False(t, status.Held)
	assert.True(t, status.HasCLI)
}

func TestReleaseLock(t *testing.T) {
	setupLockTest(t)

	_, err := AcquireLock("test-pp-cli", "scope-1", false)
	require.NoError(t, err)

	err = ReleaseLock("test-pp-cli")
	require.NoError(t, err)

	_, err = os.Stat(LockFilePath("test-pp-cli"))
	assert.True(t, os.IsNotExist(err))
}

func TestReleaseLock_Idempotent(t *testing.T) {
	setupLockTest(t)

	err := ReleaseLock("nonexistent-pp-cli")
	assert.NoError(t, err)
}

// LockFilePath must produce the same path regardless of whether the caller
// passes the slug ("notion") or the binary name ("notion-pp-cli"). A mixed-
// form caller would otherwise silently miss an active lock acquired under
// the other form.
func TestLockFilePath_NormalizesSlugAndBinaryName(t *testing.T) {
	setupLockTest(t)

	assert.Equal(t, LockFilePath("notion-pp-cli"), LockFilePath("notion"))
}

// Pin LockFilePath's normalization output across the name-form edge cases
// callers actually produce: bare slugs, binary names, legacy -cli suffix,
// rerun forms (numeric suffix on either side of -pp-cli), and the empty
// degenerate. These pin current behavior so a future refactor of the
// underlying naming helpers can't quietly drift the lock-file naming.
func TestLockFilePath_NormalizationEdgeCases(t *testing.T) {
	setupLockTest(t)

	cases := []struct {
		name     string
		input    string
		expected string
	}{
		{"bare slug appends -pp-cli", "foo", "foo-pp-cli.lock"},
		{"binary name kept verbatim", "foo-pp-cli", "foo-pp-cli.lock"},
		{"legacy -cli suffix kept verbatim", "foo-cli", "foo-cli.lock"},
		{"rerun binary form (suffix-then-number) kept", "foo-pp-cli-2", "foo-pp-cli-2.lock"},
		{"rerun library form gets -pp-cli appended", "foo-2", "foo-2-pp-cli.lock"},
		{"empty string degenerates to bare suffix", "", "-pp-cli.lock"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			assert.Equal(t, tc.expected, filepath.Base(LockFilePath(tc.input)))
		})
	}
}

// Lock helpers must reject cliName values that would escape LocksDir() or
// otherwise behave as filesystem-traversal payloads. Without this guard a
// caller passing "../foo" via `cli-printing-press lock --cli ...` would write
// the lock file outside LocksDir() once filepath.Join resolved the "..".
func TestLockHelpers_RejectInvalidCLIName(t *testing.T) {
	setupLockTest(t)

	cases := []struct {
		name  string
		input string
	}{
		{"path traversal with dotdot", "../foo"},
		{"forward slash", "foo/bar"},
		{"dotfile prefix", ".hidden"},
		{"embedded dotdot", "foo..bar"},
		{"empty string", ""},
		{"uppercase rejected by slug grammar", "FooBar"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := AcquireLock(tc.input, "scope-1", false)
			assert.Error(t, err, "AcquireLock must reject %q", tc.input)

			err = UpdateLock(tc.input, "build")
			assert.Error(t, err, "UpdateLock must reject %q", tc.input)

			err = ReleaseLock(tc.input)
			assert.Error(t, err, "ReleaseLock must reject %q", tc.input)

			status := LockStatus(tc.input)
			assert.False(t, status.Held, "LockStatus must return zero result for %q", tc.input)
			assert.False(t, status.HasCLI, "LockStatus must return zero result for %q", tc.input)
		})
	}
}

// AcquireLock with the binary name and LockStatus with the slug (or vice
// versa) must observe the same lock. This is the polish-skill safety net:
// when polish is invoked mid-pipeline, its lock check needs to detect the
// build's lock regardless of which name form polish derives.
func TestLockStatus_SeesLockAcquiredByOtherNameForm(t *testing.T) {
	setupLockTest(t)

	// Build acquires under the binary name.
	_, err := AcquireLock("notion-pp-cli", "build-scope", false)
	require.NoError(t, err)

	// Polish queries with the slug (basename of $PRESS_LIBRARY/notion).
	status := LockStatus("notion")
	assert.True(t, status.Held, "lock acquired as binary name must be visible by slug")
	assert.Equal(t, "build-scope", status.Scope)

	// And the reverse: lock acquired by slug must be visible by binary name.
	require.NoError(t, ReleaseLock("notion-pp-cli"))
	_, err = AcquireLock("notion", "polish-scope", false)
	require.NoError(t, err)

	status = LockStatus("notion-pp-cli")
	assert.True(t, status.Held, "lock acquired by slug must be visible by binary name")
	assert.Equal(t, "polish-scope", status.Scope)
}

func TestPromoteWorkingCLI(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	// Create a working directory with content.
	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	// Create a lock.
	_, err := AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	// Create minimal state.
	state := NewStateWithRun("test", workDir, "run-001", "test-scope")
	writePhase5PassForState(t, state, "none")

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)

	// Verify library dir exists with copied content (slug-keyed).
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	_, err = os.Stat(filepath.Join(libDir, "go.mod"))
	assert.NoError(t, err)
	_, err = os.Stat(filepath.Join(libDir, "main.go"))
	assert.NoError(t, err)

	// Verify lock was released.
	_, err = os.Stat(LockFilePath("test-pp-cli"))
	assert.True(t, os.IsNotExist(err))

	// Verify state was updated.
	assert.Equal(t, libDir, state.PublishedDir)
	assert.Equal(t, libDir, state.WorkingDir)
}

func TestPromoteWorkingCLI_ReplacesExistingLibrary(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	// Create existing library dir with old content (slug-keyed).
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	require.NoError(t, os.MkdirAll(libDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(libDir, "old-file.txt"), []byte("old"), 0o644))

	// Create working dir with new content.
	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "new-file.txt"), []byte("new"), 0o644))

	_, err := AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-002", "test-scope")
	writePhase5PassForState(t, state, "none")

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)

	// Old file should be gone.
	_, err = os.Stat(filepath.Join(libDir, "old-file.txt"))
	assert.True(t, os.IsNotExist(err))

	// New file should exist.
	_, err = os.Stat(filepath.Join(libDir, "new-file.txt"))
	assert.NoError(t, err)
}

func TestPromoteWorkingCLI_EmptyWorkingDir(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))

	state := NewStateWithRun("test", workDir, "run-003", "test-scope")

	err := PromoteWorkingCLI("test-pp-cli", workDir, state)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "empty")
}

func TestPromoteWorkingCLI_PreservesOldOnFailure(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	// Create existing library dir with old content (slug-keyed).
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	require.NoError(t, os.MkdirAll(libDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(libDir, "go.mod"), []byte("module old\n\ngo 1.21\n"), 0o644))

	state := NewStateWithRun("test", "/nonexistent/path", "run-004", "test-scope")

	// Promote with a nonexistent working dir should fail.
	err := PromoteWorkingCLI("test-pp-cli", "/nonexistent/path", state)
	assert.Error(t, err)

	// Old library should still be intact.
	data, readErr := os.ReadFile(filepath.Join(libDir, "go.mod"))
	require.NoError(t, readErr)
	assert.Contains(t, string(data), "module old")
}

func TestPromoteWorkingCLI_RetryRestoresBackupBeforeFailure(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	backupDir := libDir + ".old"
	stagingDir := libDir + ".promoting"

	// Simulate a crashed promote: backup survived, live library is missing,
	// and stale staging debris is still present.
	require.NoError(t, os.MkdirAll(backupDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(backupDir, "go.mod"), []byte("module old\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.MkdirAll(stagingDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(stagingDir, "partial.txt"), []byte("partial"), 0o644))

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	outside := filepath.Join(tmp, "outside.txt")
	require.NoError(t, os.WriteFile(outside, []byte("outside"), 0o644))
	require.NoError(t, os.Symlink(outside, filepath.Join(workDir, "bad-link.txt")))

	state := NewStateWithRun("test", workDir, "run-004", "test-scope")
	writePhase5PassForState(t, state, "none")

	err := PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "copying to staging directory")

	// The previous published CLI should be restored before the retry fails.
	data, readErr := os.ReadFile(filepath.Join(libDir, "go.mod"))
	require.NoError(t, readErr)
	assert.Contains(t, string(data), "module old")
	_, statErr := os.Stat(backupDir)
	assert.True(t, os.IsNotExist(statErr))
}

func TestPromoteWorkingCLI_ReleasesLockWhenStateSaveFails(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	_, err := AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-005", "test-scope")
	writePhase5PassForState(t, state, "none")

	// Force state.Save() to fail after the library swap succeeds.
	require.NoError(t, os.MkdirAll(filepath.Dir(state.PipelineDir()), 0o755))
	require.NoError(t, os.WriteFile(state.PipelineDir(), []byte("not a directory"), 0o644))

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "cli promoted to")
	assert.Contains(t, err.Error(), "state update failed")

	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	_, err = os.Stat(filepath.Join(libDir, "go.mod"))
	assert.NoError(t, err)
	_, err = os.Stat(filepath.Join(libDir, "main.go"))
	assert.NoError(t, err)

	_, err = os.Stat(LockFilePath("test-pp-cli"))
	assert.True(t, os.IsNotExist(err))
}

func TestPromoteWorkingCLI_RequiresPhase5GateForRunstatePromote(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	state := NewStateWithRun("test", workDir, "run-no-phase5", "test-scope")
	err := PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "phase5")

	_, statErr := os.Stat(filepath.Join(PublishedLibraryRoot(), "test"))
	assert.ErrorIs(t, statErr, os.ErrNotExist)
}

func TestPromoteWorkingCLI_RejectsManualPhase5Marker(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	state := NewStateWithRun("test", workDir, "run-manual-phase5", "test-scope")
	writePhase5GateMarker(t, state.ProofsDir(), Phase5AcceptanceFilename, Phase5GateMarker{
		SchemaVersion: 1,
		APIName:       state.APIName,
		RunID:         state.RunID,
		Status:        "pass",
		Level:         "manual",
		MatrixSize:    1,
		TestsPassed:   1,
		TestsFailed:   0,
		AuthContext:   Phase5AuthContext{Type: "none"},
	})

	err := PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "unknown phase5 acceptance level")
	assert.Contains(t, err.Error(), "quick, full")
	assert.Contains(t, err.Error(), "dogfood --live --write-acceptance")

	_, statErr := os.Stat(filepath.Join(PublishedLibraryRoot(), "test"))
	assert.ErrorIs(t, statErr, os.ErrNotExist)
}

func TestPromoteWorkingCLI_MinimalStateNoRunstate(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	// Create a working directory with content (simulating plan-driven CLI).
	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	// Use NewMinimalState (no RunID, no prior runstate entry).
	state := NewMinimalState("test-pp-cli", workDir)

	err := PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)

	// Verify library dir exists with copied content (slug-keyed, not CLI-named).
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	_, err = os.Stat(filepath.Join(libDir, "go.mod"))
	assert.NoError(t, err)
	_, err = os.Stat(filepath.Join(libDir, "main.go"))
	assert.NoError(t, err)

	// Verify manifest was written.
	_, err = os.Stat(filepath.Join(libDir, CLIManifestFilename))
	assert.NoError(t, err)

	// Verify state was updated with library path.
	assert.Equal(t, libDir, state.PublishedDir)
}

func TestPromoteWorkingCLI_StagesRunstateManuscripts(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	_, err := AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-stage-001", "test-scope")
	writePhase5PassForState(t, state, "none")

	// Plant research and discovery alongside the phase5 marker so we can
	// assert the whole runstate triplet gets staged into the published copy.
	require.NoError(t, os.MkdirAll(state.ResearchDir(), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(state.ResearchDir(), "notes.md"), []byte("research notes\n"), 0o644))
	require.NoError(t, os.MkdirAll(state.DiscoveryDir(), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(state.DiscoveryDir(), "endpoints.json"), []byte("{}\n"), 0o644))

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)

	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	manuRoot := filepath.Join(libDir, ".manuscripts", state.RunID)

	// phase5-acceptance.json must be reachable at the path publish validate
	// looks at first: <lib>/.manuscripts/<run-id>/proofs/.
	_, err = os.Stat(filepath.Join(manuRoot, "proofs", Phase5AcceptanceFilename))
	assert.NoError(t, err, "phase5 acceptance marker should be staged into the published copy")

	data, err := os.ReadFile(filepath.Join(manuRoot, "research", "notes.md"))
	require.NoError(t, err)
	assert.Equal(t, "research notes\n", string(data))

	data, err = os.ReadFile(filepath.Join(manuRoot, "discovery", "endpoints.json"))
	require.NoError(t, err)
	assert.Equal(t, "{}\n", string(data))
}

func TestPromoteWorkingCLI_PreservesPreexistingManuscripts(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	_, err := AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-stage-002", "test-scope")
	writePhase5PassForState(t, state, "none")

	// Working dir already carries a proofs subtree at the destination path
	// with a sentinel file. We must not clobber it during staging.
	preexistingProofs := filepath.Join(workDir, ".manuscripts", state.RunID, "proofs")
	require.NoError(t, os.MkdirAll(preexistingProofs, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(preexistingProofs, "sentinel.txt"), []byte("keep-me\n"), 0o644))

	// Also seed a different, non-overlapping research dir in runstate so
	// non-conflicting subdirs still get staged on the same run.
	require.NoError(t, os.MkdirAll(state.ResearchDir(), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(state.ResearchDir(), "notes.md"), []byte("from runstate\n"), 0o644))

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)

	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	manuRoot := filepath.Join(libDir, ".manuscripts", state.RunID)

	data, err := os.ReadFile(filepath.Join(manuRoot, "proofs", "sentinel.txt"))
	require.NoError(t, err, "pre-existing proofs subtree should survive staging")
	assert.Equal(t, "keep-me\n", string(data))

	data, err = os.ReadFile(filepath.Join(manuRoot, "research", "notes.md"))
	require.NoError(t, err)
	assert.Equal(t, "from runstate\n", string(data))
}

func TestPromoteWorkingCLI_StagesPartialRunstate(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	_, err := AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-stage-003", "test-scope")
	// Only proofs is populated — research and discovery dirs do not exist
	// in the runstate. The staging step must skip them via os.IsNotExist
	// without aborting the promote.
	writePhase5PassForState(t, state, "none")

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)

	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	manuRoot := filepath.Join(libDir, ".manuscripts", state.RunID)

	_, err = os.Stat(filepath.Join(manuRoot, "proofs", Phase5AcceptanceFilename))
	assert.NoError(t, err)

	_, statErr := os.Stat(filepath.Join(manuRoot, "research"))
	assert.True(t, os.IsNotExist(statErr), "research subdir should not be created when runstate has none")
	_, statErr = os.Stat(filepath.Join(manuRoot, "discovery"))
	assert.True(t, os.IsNotExist(statErr), "discovery subdir should not be created when runstate has none")
}

func TestPromoteWorkingCLI_StagingNoopForMinimalState(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))

	state := NewMinimalState("test-pp-cli", workDir)
	require.Empty(t, state.RunID, "minimal state should have no RunID")

	err := PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)

	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	_, statErr := os.Stat(filepath.Join(libDir, ".manuscripts"))
	assert.True(t, os.IsNotExist(statErr), "minimal-state promote should not create a .manuscripts dir")
}

func TestIsStale(t *testing.T) {
	fresh := &LockState{UpdatedAt: time.Now()}
	assert.False(t, IsStale(fresh))

	stale := &LockState{UpdatedAt: time.Now().Add(-31 * time.Minute)}
	assert.True(t, IsStale(stale))

	boundary := &LockState{UpdatedAt: time.Now().Add(-30*time.Minute - time.Second)}
	assert.True(t, IsStale(boundary))
}

func writePhase5PassForState(t *testing.T, state *PipelineState, authType string) {
	t.Helper()
	writePhase5GateMarker(t, state.ProofsDir(), Phase5AcceptanceFilename, Phase5GateMarker{
		SchemaVersion: 1,
		APIName:       state.APIName,
		RunID:         state.RunID,
		Status:        "pass",
		Level:         "full",
		MatrixSize:    1,
		TestsPassed:   1,
		TestsFailed:   0,
		AuthContext:   Phase5AuthContext{Type: authType},
	})
}

func TestConcurrentAcquire(t *testing.T) {
	setupLockTest(t)

	const goroutines = 10
	var wg sync.WaitGroup
	successes := make(chan string, goroutines)

	for i := range goroutines {
		wg.Add(1)
		scope := "scope-" + string(rune('A'+i))
		go func(s string) {
			defer wg.Done()
			_, err := AcquireLock("test-pp-cli", s, false)
			if err == nil {
				successes <- s
			}
		}(scope)
	}

	wg.Wait()
	close(successes)

	// Exactly one goroutine should have succeeded at initial acquire.
	// Others may succeed if they happen to be the same scope (unlikely)
	// or fail. At minimum one should succeed.
	winners := 0
	for range successes {
		winners++
	}
	assert.GreaterOrEqual(t, winners, 1, "at least one goroutine should acquire the lock")
}

func setLockOwnerAliveForTest(t *testing.T, alive bool) {
	t.Helper()

	original := lockOwnerAliveFunc
	lockOwnerAliveFunc = func(pid int) bool { return alive }
	t.Cleanup(func() { lockOwnerAliveFunc = original })
}

// ---------------------------------------------------------------------------
// PII gate (U3)
// ---------------------------------------------------------------------------

func TestPromoteWorkingCLI_PIIGateHaltsOnPendingFindings(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	// Plant real-shaped PII in a high-risk file
	require.NoError(t, os.WriteFile(
		filepath.Join(workDir, "data.json"),
		[]byte(`{"customer_email": "alice@example.com"}`+"\n"),
		0o644,
	))

	_, err := AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-pii-1", "test-scope")
	writePhase5PassForState(t, state, "none")

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "PII gate failed")
	assert.Contains(t, err.Error(), "pending")
	assert.Contains(t, err.Error(), "pii-polish.md")

	// Library should NOT have been created
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	_, statErr := os.Stat(libDir)
	assert.True(t, os.IsNotExist(statErr), "library dir should not exist when PII gate halts")
}

func TestPromoteWorkingCLI_PIIGatePassesWithValidAccepts(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(
		filepath.Join(workDir, "data.json"),
		[]byte(`{"customer_email": "alice@example.com"}`+"\n"),
		0o644,
	))

	// Pre-populate the ledger with the accepted finding (simulating
	// completed polish run)
	preflight, err := artifacts.FindPII(workDir)
	require.NoError(t, err)
	require.Len(t, preflight, 1)
	preflight[0].Status = artifacts.PIIStatusAccepted
	preflight[0].Category = artifacts.PIICategoryDocumentationExample
	preflight[0].EvidenceContext = "documented example in customer-email README block"
	require.NoError(t, artifacts.WritePIILedger(workDir, &artifacts.PIILedger{
		Timestamp:           time.Now().UTC(),
		CLIDir:              workDir,
		Findings:            preflight,
		FindingsCountBefore: 1,
	}))

	_, err = AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-pii-2", "test-scope")
	writePhase5PassForState(t, state, "none")

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)

	// Library should now exist
	libDir := filepath.Join(PublishedLibraryRoot(), "test")
	_, statErr := os.Stat(filepath.Join(libDir, "go.mod"))
	assert.NoError(t, statErr)

	// Ledger should be carried into the published library
	_, statErr = os.Stat(filepath.Join(libDir, artifacts.PIILedgerFilename))
	assert.NoError(t, statErr, "ledger should travel with the staged copy")
}

func TestPromoteWorkingCLI_PIIGateHaltsOnGateFailure(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	// Six findings, all accepted with identical rationale → triggers
	// duplicate-rationale gate (threshold 5)
	var lines []string
	for i := range 6 {
		lines = append(lines, `"email": "user`+string(rune('A'+i))+`@example.com"`)
	}
	require.NoError(t, os.WriteFile(
		filepath.Join(workDir, "data.json"),
		[]byte(strings.Join(lines, "\n")+"\n"),
		0o644,
	))

	preflight, err := artifacts.FindPII(workDir)
	require.NoError(t, err)
	require.Len(t, preflight, 6)
	for i := range preflight {
		preflight[i].Status = artifacts.PIIStatusAccepted
		preflight[i].Category = artifacts.PIICategoryOther
		preflight[i].EvidenceContext = "ctx"
		preflight[i].Note = "false positive"
	}
	require.NoError(t, artifacts.WritePIILedger(workDir, &artifacts.PIILedger{
		Timestamp:           time.Now().UTC(),
		CLIDir:              workDir,
		Findings:            preflight,
		FindingsCountBefore: 6,
	}))

	_, err = AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-pii-3", "test-scope")
	writePhase5PassForState(t, state, "none")

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "gate failure")
	assert.Contains(t, err.Error(), "share rationale")
}

func TestPromoteWorkingCLI_PIIGatePassesCleanDir(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("PRINTING_PRESS_HOME", tmp)
	t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
	t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)

	workDir := filepath.Join(tmp, "working", "test-pp-cli")
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test-pp-cli\n\ngo 1.21\n"), 0o644))
	require.NoError(t, os.WriteFile(
		filepath.Join(workDir, "data.json"),
		[]byte(`{"version": "1.2.3", "port": 8080}`+"\n"),
		0o644,
	))

	_, err := AcquireLock("test-pp-cli", "test-scope", false)
	require.NoError(t, err)

	state := NewStateWithRun("test", workDir, "run-pii-4", "test-scope")
	writePhase5PassForState(t, state, "none")

	err = PromoteWorkingCLI("test-pp-cli", workDir, state)
	require.NoError(t, err)
}
