//go:build linux || darwin || freebsd || windows

package target

import (
	"net"
	"os"
	"os/exec"
	"runtime"
	"slices"
	"testing"
	"time"
)

// Integration smoke tests for the target package's platform-specific
// resolution paths. They spin up real OS state (a sleeper child, a TCP
// listener) the test can identify by PID, then confirm the resolver finds it.

// startSleeper spawns a long-running child process suitable for name-resolution
// assertions. Returns the child's PID and process name; the caller must invoke
// the returned cleanup func to kill it.
func startSleeper(t *testing.T) (pid int, name string, cleanup func()) {
	t.Helper()

	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "windows":
		// `ping -n N 127.0.0.1` is a reliable cross-version Windows sleeper
		// that doesn't require a TTY (unlike `timeout /nobreak`).
		cmd = exec.Command("ping", "-n", "60", "127.0.0.1")
		name = "ping"
	default:
		cmd = exec.Command("sleep", "60")
		name = "sleep"
	}

	if err := cmd.Start(); err != nil {
		t.Skipf("could not spawn %s for integration test: %v", name, err)
	}
	return cmd.Process.Pid, name, func() {
		_ = cmd.Process.Kill()
		_, _ = cmd.Process.Wait()
	}
}

// TestIntegration_ResolveNameFindsSpawnedChild proves the name resolver talks
// to the real OS process table. We can't ResolveName against the test binary
// itself because every platform's name resolver excludes self+ancestors —
// that's a feature (so `witr bash` doesn't always match the user's own
// shell), but it forces this test to use a spawned child instead.
func TestIntegration_ResolveNameFindsSpawnedChild(t *testing.T) {
	childPID, name, cleanup := startSleeper(t)
	defer cleanup()

	// Give the OS a beat to register the child. ToolHelp32 / /proc updates
	// are usually instant, but CI runners can be sluggish.
	var pids []int
	var lastErr error
	for i := 0; i < 10; i++ {
		var err error
		pids, err = ResolveName(name, true)
		if err == nil && slices.Contains(pids, childPID) {
			return
		}
		lastErr = err
		time.Sleep(100 * time.Millisecond)
	}
	t.Errorf("ResolveName(%q) did not find spawned child PID %d after retries; got %v (last err: %v)",
		name, childPID, pids, lastErr)
}

// TestIntegration_ResolvePortFindsLoopbackListener binds a real TCP listener
// on a random localhost port and asserts ResolvePort attributes it to the
// test process. This drives the platform's port-resolution machinery end to
// end (netstat/lsof/sockstat/Win32 netstat plus /proc fd scan).
func TestIntegration_ResolvePortFindsLoopbackListener(t *testing.T) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("net.Listen: %v", err)
	}
	defer ln.Close()

	port := ln.Addr().(*net.TCPAddr).Port
	self := os.Getpid()

	pids, err := ResolvePort(port)
	if err != nil {
		t.Fatalf("ResolvePort(%d): %v", port, err)
	}
	if !slices.Contains(pids, self) {
		t.Errorf("ResolvePort(%d) = %v, missing self PID %d", port, pids, self)
	}
}

// TestIntegration_ResolvePortNonexistent asserts the resolver errors cleanly
// on a port nothing has bound. Picking a random ephemeral port and closing
// the listener gives us a port we know nobody else owns.
func TestIntegration_ResolvePortNonexistent(t *testing.T) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("net.Listen: %v", err)
	}
	port := ln.Addr().(*net.TCPAddr).Port
	ln.Close() // free the port immediately

	// The kernel may keep the port in TIME_WAIT briefly. Loop a few times
	// until the port is genuinely unowned.
	for i := 0; i < 10; i++ {
		_, err := ResolvePort(port)
		if err != nil {
			return // expected
		}
		time.Sleep(50 * time.Millisecond)
	}
	t.Errorf("ResolvePort(%d) returned nil error for an unbound port", port)
}
