package runtime

import (
	"context"
	"runtime"
	"testing"
	"time"

	"git.cloudinit.dev/coreci/orca/internal/jobspec"
)

// TestProcessRuntime_PrepareIsNoop verifies Prepare is a no-op.
func TestProcessRuntime_PrepareIsNoop(t *testing.T) {
	p := NewProcessRuntime()
	a := alloc("process", "", "/bin/true")
	if err := p.Prepare(context.Background(), a); err != nil {
		t.Fatalf("Prepare: %v", err)
	}
}

// TestProcessRuntime_PrepareNilSpec exercises the nil-spec branch
// indirectly — Prepare is a no-op regardless of input.
func TestProcessRuntime_PrepareNilSpec(t *testing.T) {
	p := NewProcessRuntime()
	if err := p.Prepare(context.Background(), &Alloc{ID: "x"}); err != nil {
		t.Fatalf("Prepare (nil spec) should still be no-op: %v", err)
	}
}

// TestProcessRuntime_StartStopStatus runs a real long-lived process
// (sleep) and verifies Start -> Status(running) -> Stop -> Status(stopped).
func TestProcessRuntime_StartStopStatus(t *testing.T) {
	if _, err := sleepBin(); err != nil {
		t.Skipf("sleep binary not available: %v", err)
	}
	p := NewProcessRuntime()
	sleepCmd, _ := sleepBin()
	a := alloc("process", "", sleepCmd+" 30")

	ctx, cancel := withTimeout(10 * time.Second)
	defer cancel()
	pid, err := p.Start(ctx, a)
	if err != nil {
		t.Fatalf("Start: %v", err)
	}
	if pid <= 0 {
		t.Fatalf("pid = %d, want > 0", pid)
	}

	st, err := p.Status(context.Background(), a)
	if err != nil {
		t.Fatalf("Status: %v", err)
	}
	if st != StateRunning {
		t.Errorf("Status = %q, want running", st)
	}

	// Verify PID lookup.
	got, err := p.PID(a.ID)
	if err != nil || got != pid {
		t.Errorf("PID = %d/%v, want %d", got, err, pid)
	}

	stopCtx, cancelStop := withTimeout(15 * time.Second)
	defer cancelStop()
	if err := p.Stop(stopCtx, a); err != nil {
		t.Fatalf("Stop: %v", err)
	}

	st2, _ := p.Status(context.Background(), a)
	if st2 != StateStopped {
		t.Errorf("Status after Stop = %q, want stopped", st2)
	}
}

// TestProcessRuntime_StopNotStarted verifies Stop is idempotent on a
// never-started alloc.
func TestProcessRuntime_StopNotStarted(t *testing.T) {
	p := NewProcessRuntime()
	a := alloc("process", "", "/bin/true")
	ctx, cancel := withTimeout(2 * time.Second)
	defer cancel()
	if err := p.Stop(ctx, a); err != nil {
		t.Errorf("Stop on never-started alloc should be no-op, got %v", err)
	}
}

// TestProcessRuntime_StatusPending verifies Status returns pending for
// an alloc that was never started.
func TestProcessRuntime_StatusPending(t *testing.T) {
	p := NewProcessRuntime()
	a := alloc("process", "", "/bin/true")
	st, err := p.Status(context.Background(), a)
	if err != nil {
		t.Fatalf("Status: %v", err)
	}
	if st != StatePending {
		t.Errorf("Status = %q, want pending", st)
	}
}

// TestProcessRuntime_StartNoCommand verifies Start errors on missing
// command.
func TestProcessRuntime_StartNoCommand(t *testing.T) {
	p := NewProcessRuntime()
	a := &Alloc{ID: "x", Spec: nil, Runtime: "process"}
	if _, err := p.Start(context.Background(), a); err == nil {
		t.Error("Start with nil spec should error")
	}
}

// TestProcessRuntime_StartEmptyCommand verifies Start errors when
// the command parses to zero argv.
func TestProcessRuntime_StartEmptyCommand(t *testing.T) {
	p := NewProcessRuntime()
	a := &Alloc{
		ID:      "e",
		Runtime: "process",
		Spec: &jobspec.WorkloadSpec{
			Runtime: &jobspec.RuntimeBlock{Command: "   "},
		},
	}
	_, err := p.Start(context.Background(), a)
	if err == nil {
		t.Error("Start with empty command should error")
	}
}

// TestProcessRuntime_StartBadBinary verifies Start propagates exec
// errors for a missing binary.
func TestProcessRuntime_StartBadBinary(t *testing.T) {
	p := NewProcessRuntime()
	a := alloc("process", "", "/no/such/binary/here")
	_, err := p.Start(context.Background(), a)
	if err == nil {
		t.Error("Start with missing binary should error")
	}
}

// TestProcessRuntime_StopOnFinishedProcess verifies Stop on a process
// that already exited (e.g. /bin/true) is a no-op (no error).
func TestProcessRuntime_StopOnFinishedProcess(t *testing.T) {
	p := NewProcessRuntime()
	a := alloc("process", "", "/bin/true")
	_, _ = p.Start(context.Background(), a)
	// Give /bin/true time to exit.
	time.Sleep(200 * time.Millisecond)
	ctx, cancel := withTimeout(5 * time.Second)
	defer cancel()
	if err := p.Stop(ctx, a); err != nil {
		t.Errorf("Stop on exited process should be no-op, got %v", err)
	}
}

// TestProcessRuntime_PIDUnknown verifies PID lookup errors for an
// unknown alloc.
func TestProcessRuntime_PIDUnknown(t *testing.T) {
	p := NewProcessRuntime()
	if _, err := p.PID("nope"); err == nil {
		t.Error("PID unknown should error")
	}
}

// sleepBin returns the sleep command path ("sleep") on this OS.
func sleepBin() (string, error) {
	// /bin/sleep exists on Linux; on other platforms fall back to
	// "sleep" (resolved via PATH).
	if runtime.GOOS == "linux" {
		return "/bin/sleep", nil
	}
	return "sleep", nil
}
