package daemoncontrol

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

	"github.com/tidbcloud/AgentTeam/x/proc"
)

func TestReceiptStoreRoundTripUsesPrivateAtomicFile(t *testing.T) {
	runtimeDir := filepath.Join(t.TempDir(), "run")
	store := NewReceiptStore(runtimeDir)
	want := validReceipt()
	want.BaseURL = "http://localhost:43123/"

	if err := store.Write(want); err != nil {
		t.Fatalf("Write() error = %v", err)
	}

	info, err := os.Stat(filepath.Join(runtimeDir, "daemon.json"))
	if err != nil {
		t.Fatalf("Stat() error = %v", err)
	}
	if got := info.Mode().Perm(); runtime.GOOS != "windows" && got != 0o600 {
		t.Fatalf("receipt mode = %#o, want 0600", got)
	}
	dirInfo, err := os.Stat(runtimeDir)
	if err != nil {
		t.Fatalf("Stat(runtime dir) error = %v", err)
	}
	if got := dirInfo.Mode().Perm(); runtime.GOOS != "windows" && got != 0o700 {
		t.Fatalf("runtime directory mode = %#o, want 0700", got)
	}
	entries, err := os.ReadDir(runtimeDir)
	if err != nil {
		t.Fatalf("ReadDir() error = %v", err)
	}
	if hasTemporaryReceipt(entries) {
		t.Fatalf("runtime directory contains temporary receipt: %v", entryNames(entries))
	}
	if !hasEntry(entries, "daemon.json") {
		t.Fatalf("runtime directory entries = %v, want daemon.json", entryNames(entries))
	}

	got, err := store.Read()
	if err != nil {
		t.Fatalf("Read() error = %v", err)
	}
	want.BaseURL = "http://127.0.0.1:43123"
	if !receiptsEqual(got, want) {
		t.Fatalf("Read() = %+v, want %+v", got, want)
	}

	data, err := os.ReadFile(filepath.Join(runtimeDir, "daemon.json"))
	if err != nil {
		t.Fatalf("ReadFile() error = %v", err)
	}
	var wire map[string]any
	if err := json.Unmarshal(data, &wire); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}
	owner, ok := wire["owner"].(map[string]any)
	if !ok {
		t.Fatalf("owner JSON = %#v, want object", wire["owner"])
	}
	for _, key := range []string{"kind", "id", "pid", "processStartedAt", "processStartId"} {
		if _, ok := owner[key]; !ok {
			t.Errorf("owner JSON missing %q: %#v", key, owner)
		}
	}
	if _, ok := owner["processIdentity"]; ok {
		t.Fatalf("owner JSON unexpectedly nests process identity: %#v", owner)
	}
}

func TestReceiptValidateStrict(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*Receipt)
	}{
		{"schema version", func(r *Receipt) { r.SchemaVersion++ }},
		{"desktop API version", func(r *Receipt) { r.DesktopAPIVersion++ }},
		{"blank instance ID", func(r *Receipt) { r.InstanceID = " \t" }},
		{"zero PID", func(r *Receipt) { r.PID = 0 }},
		{"zero process start", func(r *Receipt) { r.StartedAt = time.Time{} }},
		{"daemon start ID whitespace", func(r *Receipt) { r.StartID = " id " }},
		{"daemon start ID too long", func(r *Receipt) { r.StartID = strings.Repeat("a", 257) }},
		{"invalid owner kind", func(r *Receipt) { r.Owner.Kind = "browser" }},
		{"blank owner ID", func(r *Receipt) { r.Owner.ID = " " }},
		{"zero owner PID", func(r *Receipt) { r.Owner.PID = 0 }},
		{"zero owner start", func(r *Receipt) { r.Owner.StartedAt = time.Time{} }},
		{"owner start ID whitespace", func(r *Receipt) { r.Owner.StartID = " id " }},
		{"blank token", func(r *Receipt) { r.ControlToken = "\n" }},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			receipt := validReceipt()
			tt.mutate(&receipt)
			if err := receipt.Validate(); !errors.Is(err, ErrReceiptInvalid) {
				t.Fatalf("Validate() error = %v, want ErrReceiptInvalid", err)
			}
		})
	}

	validURLs := map[string]string{
		"http://127.0.0.1:43123":  "http://127.0.0.1:43123",
		"http://localhost:43123/": "http://127.0.0.1:43123",
		"http://[::1]:43123":      "http://127.0.0.1:43123",
	}
	for raw, want := range validURLs {
		t.Run("valid "+raw, func(t *testing.T) {
			got, err := NormalizeBaseURL(raw)
			if err != nil {
				t.Fatalf("NormalizeBaseURL() error = %v", err)
			}
			if got != want {
				t.Fatalf("NormalizeBaseURL() = %q, want %q", got, want)
			}
		})
	}

	invalidURLs := []string{
		"",
		"https://127.0.0.1:43123",
		"http://127.0.0.2:43123",
		"http://example.com:43123",
		"http://LOCALHOST:43123",
		"http://localhost",
		"http://localhost:0",
		"http://localhost:65536",
		"http://user@localhost:43123",
		"http://localhost:43123/path",
		"http://localhost:43123?query=yes",
		"http://localhost:43123/#fragment",
	}
	for _, raw := range invalidURLs {
		t.Run("invalid "+raw, func(t *testing.T) {
			if _, err := NormalizeBaseURL(raw); !errors.Is(err, ErrReceiptInvalid) {
				t.Fatalf("NormalizeBaseURL() error = %v, want ErrReceiptInvalid", err)
			}
		})
	}
}

func TestReceiptReadRejectsUnsafeOrInvalidFiles(t *testing.T) {
	tests := []struct {
		name    string
		content string
		mode    os.FileMode
		want    error
	}{
		{
			name:    "unknown field",
			content: strings.TrimSuffix(mustReceiptJSON(t, validReceipt()), "}") + `,"unexpected":true}`,
			mode:    0o600,
			want:    ErrReceiptInvalid,
		},
		{
			name:    "malformed",
			content: `{"schemaVersion":`,
			mode:    0o600,
			want:    ErrReceiptInvalid,
		},
		{
			name:    "trailing data",
			content: mustReceiptJSON(t, validReceipt()) + "\n{}",
			mode:    0o600,
			want:    ErrReceiptInvalid,
		},
		{
			name:    "broad permissions",
			content: mustReceiptJSON(t, validReceipt()),
			mode:    0o644,
			want:    ErrReceiptAmbiguous,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			runtimeDir := t.TempDir()
			path := filepath.Join(runtimeDir, "daemon.json")
			if err := os.WriteFile(path, []byte(tt.content), tt.mode); err != nil {
				t.Fatalf("WriteFile() error = %v", err)
			}
			if err := os.Chmod(path, tt.mode); err != nil {
				t.Fatalf("Chmod() error = %v", err)
			}
			if tt.name != "broad permissions" {
				if err := secureRuntimeFile(path); err != nil {
					t.Fatalf("secureRuntimeFile() error = %v", err)
				}
			}
			_, err := NewReceiptStore(runtimeDir).Read()
			if !errors.Is(err, tt.want) {
				t.Fatalf("Read() error = %v, want %v", err, tt.want)
			}
			info, statErr := os.Stat(path)
			if statErr != nil {
				t.Fatalf("Stat() error = %v", statErr)
			}
			if got := info.Mode().Perm(); runtime.GOOS != "windows" && got != tt.mode {
				t.Fatalf("Read() changed mode to %#o, want %#o", got, tt.mode)
			}
		})
	}
}

func TestReceiptReadErrorDoesNotLeakControlToken(t *testing.T) {
	const secret = "malformed-secret-control-token"
	runtimeDir := t.TempDir()
	content := mustReceiptJSON(t, validReceipt())
	content = strings.Replace(content, `"control-token-a"`, `"`+secret+`"`, 1) + " trailing"
	if err := os.WriteFile(filepath.Join(runtimeDir, "daemon.json"), []byte(content), 0o600); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}
	if err := secureRuntimeFile(filepath.Join(runtimeDir, "daemon.json")); err != nil {
		t.Fatalf("secureRuntimeFile() error = %v", err)
	}

	_, err := NewReceiptStore(runtimeDir).Read()
	if !errors.Is(err, ErrReceiptInvalid) {
		t.Fatalf("Read() error = %v, want ErrReceiptInvalid", err)
	}
	if strings.Contains(err.Error(), secret) {
		t.Fatalf("Read() leaked control token: %v", err)
	}
}

func TestReceiptReadMissingAndNonRegular(t *testing.T) {
	runtimeDir := t.TempDir()
	store := NewReceiptStore(runtimeDir)
	if _, err := store.Read(); !errors.Is(err, ErrReceiptMissing) {
		t.Fatalf("Read() missing error = %v, want ErrReceiptMissing", err)
	}

	path := filepath.Join(runtimeDir, "daemon.json")
	if err := os.Mkdir(path, 0o700); err != nil {
		t.Fatalf("Mkdir() error = %v", err)
	}
	if _, err := store.Read(); !errors.Is(err, ErrReceiptAmbiguous) {
		t.Fatalf("Read() directory error = %v, want ErrReceiptAmbiguous", err)
	}
}

func TestReceiptDiagnosticsRedactControlToken(t *testing.T) {
	receipt := validReceipt()
	const secret = "super-secret-control-token"
	receipt.ControlToken = secret

	for name, diagnostic := range map[string]string{
		"String":         receipt.String(),
		"DiagnosticJSON": receipt.DiagnosticJSON(),
	} {
		if strings.Contains(diagnostic, secret) {
			t.Fatalf("%s leaked control token: %s", name, diagnostic)
		}
		if !strings.Contains(diagnostic, "[REDACTED]") {
			t.Fatalf("%s did not mark token redacted: %s", name, diagnostic)
		}
		if !json.Valid([]byte(diagnostic)) {
			t.Fatalf("%s is not valid JSON: %s", name, diagnostic)
		}
	}

	receipt.InstanceID = " "
	if err := receipt.Validate(); err == nil || strings.Contains(err.Error(), secret) {
		t.Fatalf("Validate() error = %v, want non-leaking validation error", err)
	}
}

func TestDeleteMatchingUsesReceiptIdentityCAS(t *testing.T) {
	store := NewReceiptStore(t.TempDir())
	receipt := validReceipt()
	if err := store.Write(receipt); err != nil {
		t.Fatalf("Write() error = %v", err)
	}

	wrongIdentity := receipt.Identity()
	wrongIdentity.StartedAt = wrongIdentity.StartedAt.Add(time.Second)
	if err := store.DeleteMatching(receipt.InstanceID, wrongIdentity); !errors.Is(err, ErrReceiptMismatch) {
		t.Fatalf("DeleteMatching(wrong identity) error = %v, want ErrReceiptMismatch", err)
	}
	if _, err := store.Read(); err != nil {
		t.Fatalf("Read() after refused deletion error = %v", err)
	}

	if err := store.DeleteMatching("another-instance", receipt.Identity()); !errors.Is(err, ErrReceiptMismatch) {
		t.Fatalf("DeleteMatching(wrong instance) error = %v, want ErrReceiptMismatch", err)
	}
	if err := store.DeleteMatching(receipt.InstanceID, receipt.Identity()); err != nil {
		t.Fatalf("DeleteMatching() error = %v", err)
	}
	if _, err := store.Read(); !errors.Is(err, ErrReceiptMissing) {
		t.Fatalf("Read() after deletion error = %v, want ErrReceiptMissing", err)
	}
}

func TestReconcileRefusesLiveDaemon(t *testing.T) {
	store := NewReceiptStore(t.TempDir())
	receipt := validReceipt()
	if err := store.Write(receipt); err != nil {
		t.Fatalf("Write() error = %v", err)
	}

	err := store.Reconcile(func(pid int) (proc.ProcessInfo, bool) {
		return proc.ProcessInfo{
			PID:       pid,
			StartedAt: receipt.StartedAt.Add(-7 * time.Second),
			StartID:   receipt.StartID,
		}, true
	})
	if !errors.Is(err, ErrDaemonAlreadyRunning) {
		t.Fatalf("Reconcile() error = %v, want ErrDaemonAlreadyRunning", err)
	}
	if _, err := store.Read(); err != nil {
		t.Fatalf("Read() after live reconcile error = %v", err)
	}
}

func TestReconcileDeletesDeadOrReusedPID(t *testing.T) {
	tests := []struct {
		name   string
		lookup func(Receipt) func(int) (proc.ProcessInfo, bool)
	}{
		{
			name: "dead",
			lookup: func(Receipt) func(int) (proc.ProcessInfo, bool) {
				return func(int) (proc.ProcessInfo, bool) { return proc.ProcessInfo{}, false }
			},
		},
		{
			name: "PID reused",
			lookup: func(receipt Receipt) func(int) (proc.ProcessInfo, bool) {
				return func(pid int) (proc.ProcessInfo, bool) {
					return proc.ProcessInfo{PID: pid, StartedAt: receipt.StartedAt.Add(time.Second)}, true
				}
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			store := NewReceiptStore(t.TempDir())
			receipt := validReceipt()
			receipt.PID = 2_000_000_000
			if err := store.Write(receipt); err != nil {
				t.Fatalf("Write() error = %v", err)
			}
			if err := store.Reconcile(tt.lookup(receipt)); err != nil {
				t.Fatalf("Reconcile() error = %v", err)
			}
			if _, err := store.Read(); !errors.Is(err, ErrReceiptMissing) {
				t.Fatalf("Read() error = %v, want ErrReceiptMissing", err)
			}
		})
	}
}

func TestReconcileRecognizesLegacyReceiptOnlyForSafeCleanup(t *testing.T) {
	tests := []struct {
		name       string
		apiVersion int
		lookup     func(Receipt) func(int) (proc.ProcessInfo, bool)
		wantErr    error
		wantExists bool
	}{
		{
			name:       "dead legacy daemon",
			apiVersion: legacyDesktopAPIVersion,
			lookup: func(Receipt) func(int) (proc.ProcessInfo, bool) {
				return func(int) (proc.ProcessInfo, bool) { return proc.ProcessInfo{}, false }
			},
		},
		{
			name:       "reused legacy daemon PID",
			apiVersion: legacyDesktopAPIVersion,
			lookup: func(receipt Receipt) func(int) (proc.ProcessInfo, bool) {
				return func(pid int) (proc.ProcessInfo, bool) {
					return proc.ProcessInfo{PID: pid, StartedAt: receipt.StartedAt.Add(time.Second)}, true
				}
			},
		},
		{
			name:       "live legacy daemon",
			apiVersion: legacyDesktopAPIVersion,
			lookup: func(receipt Receipt) func(int) (proc.ProcessInfo, bool) {
				return func(pid int) (proc.ProcessInfo, bool) {
					return proc.ProcessInfo{
						PID:       pid,
						StartedAt: receipt.StartedAt,
						StartID:   receipt.StartID,
					}, true
				}
			},
			wantErr:    ErrDaemonAlreadyRunning,
			wantExists: true,
		},
		{
			name:       "unknown future API",
			apiVersion: DesktopAPIVersion + 1,
			lookup: func(Receipt) func(int) (proc.ProcessInfo, bool) {
				return func(int) (proc.ProcessInfo, bool) { return proc.ProcessInfo{}, false }
			},
			wantErr:    ErrReceiptInvalid,
			wantExists: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			runtimeDir := t.TempDir()
			store := NewReceiptStore(runtimeDir)
			receipt := validReceipt()
			receipt.DesktopAPIVersion = tt.apiVersion
			receipt.PID = 2_000_000_000
			path := filepath.Join(runtimeDir, receiptFilename)
			if err := os.WriteFile(path, []byte(mustReceiptJSON(t, receipt)), 0o600); err != nil {
				t.Fatalf("WriteFile() error = %v", err)
			}
			if err := secureRuntimeFile(path); err != nil {
				t.Fatalf("secureRuntimeFile() error = %v", err)
			}
			if _, err := store.Read(); !errors.Is(err, ErrReceiptInvalid) {
				t.Fatalf("Read() error = %v, want ErrReceiptInvalid", err)
			}

			err := store.Reconcile(tt.lookup(receipt))
			if !errors.Is(err, tt.wantErr) {
				t.Fatalf("Reconcile() error = %v, want %v", err, tt.wantErr)
			}
			_, statErr := os.Stat(path)
			if tt.wantExists && statErr != nil {
				t.Fatalf("Stat() error = %v, want receipt to remain", statErr)
			}
			if !tt.wantExists && !errors.Is(statErr, os.ErrNotExist) {
				t.Fatalf("Stat() error = %v, want receipt removed", statErr)
			}
			if !tt.wantExists {
				replacement := validReceipt()
				replacement.InstanceID = "replacement-instance"
				if err := store.Write(replacement); err != nil {
					t.Fatalf("Write(replacement) error = %v", err)
				}
				got, err := store.Read()
				if err != nil || got.DesktopAPIVersion != DesktopAPIVersion {
					t.Fatalf("Read(replacement) = %+v, %v", got, err)
				}
			}
		})
	}
}

func TestReconcileLegacyReceiptWithAmbiguousLivePIDFailsClosed(t *testing.T) {
	runtimeDir := t.TempDir()
	store := NewReceiptStore(runtimeDir)
	receipt := validReceipt()
	receipt.DesktopAPIVersion = legacyDesktopAPIVersion
	receipt.PID = os.Getpid()
	path := filepath.Join(runtimeDir, receiptFilename)
	if err := os.WriteFile(path, []byte(mustReceiptJSON(t, receipt)), 0o600); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}
	if err := secureRuntimeFile(path); err != nil {
		t.Fatalf("secureRuntimeFile() error = %v", err)
	}

	err := store.Reconcile(func(int) (proc.ProcessInfo, bool) {
		return proc.ProcessInfo{}, false
	})
	if !errors.Is(err, ErrReceiptAmbiguous) {
		t.Fatalf("Reconcile() error = %v, want ErrReceiptAmbiguous", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Fatalf("Stat() error = %v, want receipt to remain", err)
	}
}

func TestDeleteMatchingDoesNotAcceptLegacyReceipt(t *testing.T) {
	runtimeDir := t.TempDir()
	store := NewReceiptStore(runtimeDir)
	receipt := validReceipt()
	receipt.DesktopAPIVersion = legacyDesktopAPIVersion
	path := filepath.Join(runtimeDir, receiptFilename)
	if err := os.WriteFile(path, []byte(mustReceiptJSON(t, receipt)), 0o600); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}
	if err := secureRuntimeFile(path); err != nil {
		t.Fatalf("secureRuntimeFile() error = %v", err)
	}

	err := store.DeleteMatching(receipt.InstanceID, receipt.Identity())
	if !errors.Is(err, ErrReceiptInvalid) {
		t.Fatalf("DeleteMatching() error = %v, want ErrReceiptInvalid", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Fatalf("Stat() error = %v, want receipt to remain", err)
	}
}

func TestReconcileLookupUnknownFailsClosedForSeeminglyLivePID(t *testing.T) {
	store := NewReceiptStore(t.TempDir())
	receipt := validReceipt()
	receipt.PID = os.Getpid()
	if err := store.Write(receipt); err != nil {
		t.Fatalf("Write() error = %v", err)
	}

	err := store.Reconcile(func(int) (proc.ProcessInfo, bool) {
		return proc.ProcessInfo{}, false
	})
	if !errors.Is(err, ErrReceiptAmbiguous) {
		t.Fatalf("Reconcile() error = %v, want ErrReceiptAmbiguous", err)
	}
	if _, err := store.Read(); err != nil {
		t.Fatalf("Read() after unknown lookup error = %v", err)
	}
}

func TestReconcileIncompleteLookupFailsClosed(t *testing.T) {
	tests := []proc.ProcessInfo{
		{PID: 4242},
		{PID: 4243, StartedAt: time.Date(2026, 7, 24, 7, 8, 9, 123_000_000, time.UTC)},
	}
	for _, process := range tests {
		t.Run(processIdentityName(process), func(t *testing.T) {
			store := NewReceiptStore(t.TempDir())
			receipt := validReceipt()
			if err := store.Write(receipt); err != nil {
				t.Fatalf("Write() error = %v", err)
			}
			err := store.Reconcile(func(int) (proc.ProcessInfo, bool) {
				return process, true
			})
			if !errors.Is(err, ErrReceiptAmbiguous) {
				t.Fatalf("Reconcile() error = %v, want ErrReceiptAmbiguous", err)
			}
			if _, err := store.Read(); err != nil {
				t.Fatalf("Read() after ambiguous reconcile error = %v", err)
			}
		})
	}
}

func TestReconcileStaleDeletionRereadsReceipt(t *testing.T) {
	store := NewReceiptStore(t.TempDir())
	receipt := validReceipt()
	if err := store.Write(receipt); err != nil {
		t.Fatalf("Write() error = %v", err)
	}

	err := store.Reconcile(func(pid int) (proc.ProcessInfo, bool) {
		if err := os.Remove(filepath.Join(store.runtimeDir, "daemon.json")); err != nil {
			t.Fatalf("Remove() error = %v", err)
		}
		replacement := validReceipt()
		replacement.InstanceID = "replacement-instance"
		replacement.PID = pid + 1
		if err := store.Write(replacement); err != nil {
			t.Fatalf("Write(replacement) error = %v", err)
		}
		return proc.ProcessInfo{PID: pid, StartedAt: receipt.StartedAt.Add(time.Second)}, true
	})
	if !errors.Is(err, ErrReceiptMismatch) {
		t.Fatalf("Reconcile() error = %v, want ErrReceiptMismatch", err)
	}
	got, readErr := store.Read()
	if readErr != nil {
		t.Fatalf("Read() error = %v", readErr)
	}
	if got.InstanceID != "replacement-instance" {
		t.Fatalf("replacement receipt was deleted: %+v", got)
	}
}

func TestConcurrentWritePublishesExactlyOneReceipt(t *testing.T) {
	runtimeDir := t.TempDir()
	stores := []*ReceiptStore{
		NewReceiptStore(runtimeDir),
		NewReceiptStore(runtimeDir),
	}
	receipts := []Receipt{validReceipt(), validReceipt()}
	receipts[1].InstanceID = "instance-b"
	receipts[1].ControlToken = "token-b"

	start := make(chan struct{})
	errs := make(chan error, len(receipts))
	var wg sync.WaitGroup
	for index, receipt := range receipts {
		store := stores[index]
		receipt := receipt
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			errs <- store.Write(receipt)
		}()
	}
	close(start)
	wg.Wait()
	close(errs)

	var successes, alreadyRunning int
	for err := range errs {
		switch {
		case err == nil:
			successes++
		case errors.Is(err, ErrDaemonAlreadyRunning):
			alreadyRunning++
		default:
			t.Fatalf("Write() unexpected error = %v", err)
		}
	}
	if successes != 1 || alreadyRunning != 1 {
		t.Fatalf("concurrent results = %d success, %d already running; want 1 and 1", successes, alreadyRunning)
	}
	entries, err := os.ReadDir(runtimeDir)
	if err != nil {
		t.Fatalf("ReadDir() error = %v", err)
	}
	if hasTemporaryReceipt(entries) {
		t.Fatalf("runtime directory contains temporary receipt: %v", entryNames(entries))
	}
	if !hasEntry(entries, "daemon.json") {
		t.Fatalf("runtime directory entries = %v, want daemon.json", entryNames(entries))
	}
}

func TestRewriteMatchingAtomicallyRotatesOwnerAndToken(t *testing.T) {
	store := NewReceiptStore(t.TempDir())
	receipt := validReceipt()
	if err := store.Write(receipt); err != nil {
		t.Fatalf("Write() error = %v", err)
	}

	replacement := receipt
	replacement.Owner.ID = "owner-b"
	replacement.Owner.PID++
	replacement.Owner.StartedAt = replacement.Owner.StartedAt.Add(time.Second)
	replacement.Owner.StartID = "linux:boot-a:456"
	replacement.ControlToken = "control-token-b"
	if err := store.RewriteMatching(receipt, replacement); err != nil {
		t.Fatalf("RewriteMatching() error = %v", err)
	}
	got, err := store.Read()
	if err != nil {
		t.Fatalf("Read() error = %v", err)
	}
	if !sameReceipt(got, replacement) {
		t.Fatalf("rewritten receipt = %+v, want %+v", got, replacement)
	}
}

func TestRewriteMatchingFailurePreservesCurrentReceipt(t *testing.T) {
	store := NewReceiptStore(t.TempDir())
	receipt := validReceipt()
	if err := store.Write(receipt); err != nil {
		t.Fatalf("Write() error = %v", err)
	}
	wrong := receipt
	wrong.ControlToken = "wrong-old-token"
	replacement := receipt
	replacement.ControlToken = "new-token"

	err := store.RewriteMatching(wrong, replacement)
	if !errors.Is(err, ErrReceiptMismatch) {
		t.Fatalf("RewriteMatching() error = %v, want ErrReceiptMismatch", err)
	}
	got, readErr := store.Read()
	if readErr != nil {
		t.Fatalf("Read() error = %v", readErr)
	}
	if !sameReceipt(got, receipt) {
		t.Fatalf("receipt changed after failed rewrite: %+v", got)
	}
}

func TestWriteUsesPreexistingUnlockedMutationLockFile(t *testing.T) {
	runtimeDir := t.TempDir()
	lockPath := filepath.Join(runtimeDir, "."+receiptFilename+".write.lock")
	if err := os.WriteFile(lockPath, nil, 0o600); err != nil {
		t.Fatalf("WriteFile(lock) error = %v", err)
	}

	store := NewReceiptStore(runtimeDir)
	if err := store.Write(validReceipt()); err != nil {
		t.Fatalf("Write() with preexisting unlocked lock file error = %v", err)
	}
	if _, err := store.Read(); err != nil {
		t.Fatalf("Read() error = %v", err)
	}
}

func validReceipt() Receipt {
	daemonStartedAt := time.Date(2026, 7, 24, 7, 8, 9, 123_000_000, time.UTC)
	ownerStartedAt := daemonStartedAt.Add(-time.Minute)
	return Receipt{
		SchemaVersion:     SchemaVersion,
		DesktopAPIVersion: DesktopAPIVersion,
		InstanceID:        "instance-a",
		PID:               4242,
		StartedAt:         daemonStartedAt,
		StartID:           "linux:boot-a:123",
		BaseURL:           "http://127.0.0.1:43123",
		Owner: Owner{
			Kind: OwnerKindElectron,
			ID:   "electron-main",
			ProcessIdentity: ProcessIdentity{
				PID:       4241,
				StartedAt: ownerStartedAt,
				StartID:   "linux:boot-a:100",
			},
		},
		ControlToken: "control-token-a",
	}
}

func mustReceiptJSON(t *testing.T, receipt Receipt) string {
	t.Helper()
	data, err := json.Marshal(receipt)
	if err != nil {
		t.Fatalf("Marshal() error = %v", err)
	}
	return string(data)
}

func receiptsEqual(a, b Receipt) bool {
	return a.SchemaVersion == b.SchemaVersion &&
		a.DesktopAPIVersion == b.DesktopAPIVersion &&
		a.InstanceID == b.InstanceID &&
		a.PID == b.PID &&
		a.StartedAt.Equal(b.StartedAt) &&
		a.StartID == b.StartID &&
		a.BaseURL == b.BaseURL &&
		a.Owner.Kind == b.Owner.Kind &&
		a.Owner.ID == b.Owner.ID &&
		a.Owner.PID == b.Owner.PID &&
		a.Owner.StartedAt.Equal(b.Owner.StartedAt) &&
		a.Owner.StartID == b.Owner.StartID &&
		a.ControlToken == b.ControlToken
}

func entryNames(entries []os.DirEntry) []string {
	names := make([]string, 0, len(entries))
	for _, entry := range entries {
		names = append(names, entry.Name())
	}
	return names
}

func hasEntry(entries []os.DirEntry, name string) bool {
	for _, entry := range entries {
		if entry.Name() == name {
			return true
		}
	}
	return false
}

func hasTemporaryReceipt(entries []os.DirEntry) bool {
	for _, entry := range entries {
		if strings.HasPrefix(entry.Name(), "."+receiptFilename+".tmp-") {
			return true
		}
	}
	return false
}

func processIdentityName(process proc.ProcessInfo) string {
	if process.StartedAt.IsZero() {
		return "missing start time"
	}
	return "wrong PID"
}
