package runner

import (
	"context"
	"strings"
	"testing"
	"time"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
)

// (fakeSender is defined in runner_test.go; it's already lock-protected
// because the runner's stdout + stderr pump goroutines call Send
// concurrently. The original local 'safeSender' here was a workaround
// from before fakeSender itself grew the mutex.)

// TestRunBackupCanceledMidRunReportsCanceled spawns a backup against
// a fake restic that sleeps for 30 seconds, cancels the context after
// a short delay, and confirms the resulting job.finished envelope
// reports status=canceled (not failed).
func TestRunBackupCanceledMidRunReportsCanceled(t *testing.T) {
	t.Parallel()

	// Fake restic: replace the shell with a long sleep via `exec` so the
	// process tree is one process — SIGTERM goes directly to sleep and
	// it exits. Without `exec`, the shell stays in the foreground while
	// sleep is its child; SIGTERM-to-shell may or may not propagate to
	// sleep depending on the shell, leading to the WaitDelay-then-
	// SIGKILL fallback path firing — slower and noisier.
	bin := setupScript(t, `exec sleep 30`)

	tx := &fakeSender{}
	r := New(Config{ResticBin: bin}, tx, 0)

	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan error, 1)
	go func() {
		done <- r.RunBackup(ctx, "job-cancel", []string{"/tmp/x"}, nil, nil, BackupHooks{})
	}()

	// Wait long enough for the subprocess to actually start before
	// canceling. Without this, exec.CommandContext can race the
	// kill against Start and produce a different error path.
	time.Sleep(150 * time.Millisecond)
	cancel()

	select {
	case <-done:
	case <-time.After(15 * time.Second):
		t.Fatal("RunBackup did not return within 15s of cancel")
	}

	// Locate the job.finished envelope and check its status.
	envs := tx.snapshot()
	var finEnv api.Envelope
	var found bool
	for _, e := range envs {
		if e.Type == api.MsgJobFinished {
			finEnv = e
			found = true
			break
		}
	}
	if !found {
		t.Fatal("no job.finished envelope was sent")
	}
	var fin api.JobFinishedPayload
	if err := finEnv.UnmarshalPayload(&fin); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if fin.Status != api.JobCancelled {
		t.Fatalf("status: got %q, want %q", fin.Status, api.JobCancelled)
	}
	if fin.ExitCode != 130 {
		t.Errorf("exit_code: got %d, want 130 (POSIX cancel convention)", fin.ExitCode)
	}
	// The error message should be empty for canceled jobs (see runner.sendFinished).
	if !strings.HasPrefix(fin.Error, "") || fin.Error != "" {
		t.Errorf("error: got %q, want empty for canceled jobs", fin.Error)
	}
}
