package proc

import (
	"path/filepath"
	"testing"
	"time"
)

func TestParseProcessList(t *testing.T) {
	input := []byte("  100  1  100 Sun Jul 19 15:00:01 2026 /sbin/launchd\n" +
		"200 100 200 Sun Jul 19 15:01:02 2026 /bin/foo\n" +
		"bad line\n" +
		"300 200 200 Sun Jul 19 15:02:03 2026 /bin/bar arg1 arg2\n")
	procs := parseProcessList(input)
	if len(procs) != 3 {
		t.Fatalf("got %d processes, want 3", len(procs))
	}
	if procs[0].PID != 100 || procs[0].PPID != 1 || procs[0].PGID != 100 {
		t.Errorf("procs[0] = %+v", procs[0])
	}
	if procs[2].Command != "/bin/bar arg1 arg2" {
		t.Errorf("procs[2].Command = %q", procs[2].Command)
	}
	wantStartedAt, _ := time.ParseInLocation("2006-01-02 15:04:05", "2026-07-19 15:02:03", time.Local)
	if !procs[2].StartedAt.Equal(wantStartedAt) {
		t.Errorf("procs[2].StartedAt = %v, want %v", procs[2].StartedAt, wantStartedAt)
	}
}

func TestCollectDescendantPIDs(t *testing.T) {
	procs := []ProcessInfo{
		{PID: 1, PPID: 0, PGID: 1},
		{PID: 10, PPID: 1, PGID: 10},
		{PID: 20, PPID: 10, PGID: 10},
		{PID: 30, PPID: 20, PGID: 10},
		{PID: 40, PPID: 999, PGID: 40}, // unrelated
	}
	got := CollectDescendantPIDs(1, procs)
	want := map[int]bool{10: true, 20: true, 30: true}
	if len(got) != len(want) {
		t.Fatalf("got %v, want %v", got, want)
	}
	for _, pid := range got {
		if !want[pid] {
			t.Errorf("unexpected pid %d", pid)
		}
	}
}

func TestCollectDescendantPIDsNoChildren(t *testing.T) {
	procs := []ProcessInfo{{PID: 1, PPID: 0, PGID: 1}}
	got := CollectDescendantPIDs(1, procs)
	if len(got) != 0 {
		t.Errorf("got %v, want empty", got)
	}
}

func TestRepoDevProcessTargetsIgnoresRetiredMeshXServer(t *testing.T) {
	repoRoot := t.TempDir()
	binDir := filepath.Join(repoRoot, ".data", "dev")
	processes := []ProcessInfo{
		{PID: 10, Command: filepath.Join(binDir, "agentteam-server")},
		{PID: 20, Command: filepath.Join(binDir, "meshx-server")},
	}

	got := RepoDevProcessTargets(repoRoot, processes)
	if len(got) != 1 || got[0].PID != 10 {
		t.Fatalf("RepoDevProcessTargets() = %+v, want only agentteam-server", got)
	}
}

func TestProcessIdentityMatches(t *testing.T) {
	startedAt := time.Date(2026, 7, 24, 12, 34, 56, 0, time.UTC)
	tests := []struct {
		name string
		a    ProcessInfo
		b    ProcessInfo
		want bool
	}{
		{
			name: "same identity",
			a:    ProcessInfo{PID: 42, StartedAt: startedAt},
			b:    ProcessInfo{PID: 42, StartedAt: startedAt},
			want: true,
		},
		{
			name: "same PID different start",
			a:    ProcessInfo{PID: 42, StartedAt: startedAt},
			b:    ProcessInfo{PID: 42, StartedAt: startedAt.Add(time.Nanosecond)},
		},
		{
			name: "same native start ignores wall clock drift",
			a: ProcessInfo{
				PID:       42,
				StartedAt: startedAt,
				StartID:   "linux:boot-a:1234",
			},
			b: ProcessInfo{
				PID:       42,
				StartedAt: startedAt.Add(-7 * time.Second),
				StartID:   "linux:boot-a:1234",
			},
			want: true,
		},
		{
			name: "same PID and timestamp different native start",
			a:    ProcessInfo{PID: 42, StartedAt: startedAt, StartID: "linux:boot-a:1234"},
			b:    ProcessInfo{PID: 42, StartedAt: startedAt, StartID: "linux:boot-b:1234"},
		},
		{
			name: "legacy timestamp matches current native identity",
			a:    ProcessInfo{PID: 42, StartedAt: startedAt},
			b:    ProcessInfo{PID: 42, StartedAt: startedAt, StartID: "linux:boot-a:1234"},
			want: true,
		},
		{
			name: "first start unknown",
			a:    ProcessInfo{PID: 42},
			b:    ProcessInfo{PID: 42, StartedAt: startedAt},
		},
		{
			name: "second start unknown",
			a:    ProcessInfo{PID: 42, StartedAt: startedAt},
			b:    ProcessInfo{PID: 42},
		},
		{
			name: "different PID",
			a:    ProcessInfo{PID: 41, StartedAt: startedAt},
			b:    ProcessInfo{PID: 42, StartedAt: startedAt},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := ProcessIdentityMatches(tt.a, tt.b); got != tt.want {
				t.Fatalf("ProcessIdentityMatches(%+v, %+v) = %v, want %v", tt.a, tt.b, got, tt.want)
			}
		})
	}
}

func TestProcessByPIDFailsClosedWhenNativeIdentityIsRequired(t *testing.T) {
	fallbackCalled := false
	process, ok := processByPIDWithFallback(
		42,
		func(int) (ProcessInfo, bool) { return ProcessInfo{}, false },
		true,
		func() []ProcessInfo {
			fallbackCalled = true
			return []ProcessInfo{{
				PID:       42,
				StartedAt: time.Date(2026, 7, 24, 12, 34, 56, 0, time.UTC),
			}}
		},
	)
	if ok || process != (ProcessInfo{}) {
		t.Fatalf("processByPIDWithFallback = (%+v, %v), want zero, false", process, ok)
	}
	if fallbackCalled {
		t.Fatal("fallback process list was called after native identity failed")
	}
}
