package cli

import (
	"bytes"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/aoagents/agent-orchestrator/backend/internal/daemonmeta"
	"github.com/aoagents/agent-orchestrator/backend/internal/runfile"
)

func TestRootHelpDoesNotShowDaemon(t *testing.T) {
	out, _, err := executeCLI(t, Deps{}, "--help")
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(out, "\n  daemon") {
		t.Fatalf("hidden daemon command leaked into help:\n%s", out)
	}
	for _, want := range []string{"start", "stop", "status", "doctor", "completion", "version"} {
		if !strings.Contains(out, want) {
			t.Fatalf("help missing %q:\n%s", want, out)
		}
	}
}

func TestCommandsRejectUnexpectedArgs(t *testing.T) {
	for _, args := range [][]string{
		{"daemon", "extra"},
		{"start", "extra"},
		{"stop", "extra"},
		{"status", "extra"},
		{"doctor", "extra"},
		{"version", "extra"},
	} {
		t.Run(strings.Join(args, " "), func(t *testing.T) {
			_, _, err := executeCLI(t, Deps{}, args...)
			if err == nil {
				t.Fatal("expected usage error")
			}
			if got := ExitCode(err); got != 2 {
				t.Fatalf("ExitCode(%v) = %d, want 2", err, got)
			}
		})
	}
}

func TestVersionEmitsCLIInvocationBestEffort(t *testing.T) {
	cfg := setConfigEnv(t)
	called := make(chan string, 1)
	if err := runfile.Write(cfg.runFile, runfile.Info{PID: os.Getpid(), Port: 3001, StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	if _, _, err := executeCLI(t, Deps{
		HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
			if req.URL.Path == "/internal/telemetry/cli-invoked" {
				called <- req.URL.Path
				return jsonResponse(http.StatusAccepted, ""), nil
			}
			return jsonResponse(http.StatusNotFound, ""), nil
		})},
		ProcessAlive: func(pid int) bool { return pid == os.Getpid() },
	}, "version"); err != nil {
		t.Fatal(err)
	}
	select {
	case path := <-called:
		if path != "/internal/telemetry/cli-invoked" {
			t.Fatalf("telemetry path = %q, want /internal/telemetry/cli-invoked", path)
		}
	default:
		t.Fatal("version did not emit CLI invocation")
	}
}

func TestUsageErrorEmitsCLIUsageTelemetryBestEffort(t *testing.T) {
	cfg := setConfigEnv(t)
	called := make(chan string, 1)
	if err := runfile.Write(cfg.runFile, runfile.Info{PID: os.Getpid(), Port: 3001, StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	deps := Deps{
		HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
			if req.URL.Path == "/internal/telemetry/cli-usage-error" {
				called <- req.URL.Path
				return jsonResponse(http.StatusAccepted, ""), nil
			}
			return jsonResponse(http.StatusNotFound, ""), nil
		})},
		ProcessAlive: func(pid int) bool { return pid == os.Getpid() },
	}
	err := executeWithDeps(deps, []string{"status", "extra"})
	if err == nil {
		t.Fatal("expected usage error")
	}
	select {
	case path := <-called:
		if path != "/internal/telemetry/cli-usage-error" {
			t.Fatalf("telemetry path = %q, want /internal/telemetry/cli-usage-error", path)
		}
	default:
		t.Fatal("usage error did not emit CLI usage telemetry")
	}
}

func TestStatusStoppedJSON(t *testing.T) {
	setConfigEnv(t)

	out, _, err := executeCLI(t, Deps{ProcessAlive: func(int) bool { return false }}, "status", "--json")
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(out, `"state": "stopped"`) {
		t.Fatalf("status did not report stopped:\n%s", out)
	}
	if strings.Contains(out, "startedAt") {
		t.Fatalf("stopped JSON should omit startedAt:\n%s", out)
	}
}

func TestStartReturnsExistingReadyDaemon(t *testing.T) {
	cfg := setConfigEnv(t)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/healthz":
			_, _ = fmt.Fprintf(w, `{"status":"ok","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())
		case "/readyz":
			_, _ = fmt.Fprintf(w, `{"status":"ready","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	port := serverPort(t, srv.URL)
	if err := runfile.Write(cfg.runFile, runfile.Info{PID: os.Getpid(), Port: port, StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	var started bool
	out, _, err := executeCLI(t, Deps{
		ProcessAlive: func(pid int) bool { return pid == os.Getpid() },
		StartProcess: func(processStartConfig) error {
			started = true
			return nil
		},
		Now: func() time.Time { return time.Unix(110, 0).UTC() },
	}, "start", "--json")
	if err != nil {
		t.Fatal(err)
	}
	if started {
		t.Fatal("start should not spawn when daemon is already ready")
	}
	if !strings.Contains(out, `"state": "ready"`) {
		t.Fatalf("start did not report ready:\n%s", out)
	}
}

func TestStartClearsStaleRunFileBeforeSpawning(t *testing.T) {
	cfg := setConfigEnv(t)
	var spawned atomic.Bool

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !spawned.Load() {
			_, _ = fmt.Fprintf(w, `{"status":"ok","service":"not-ao","pid":4242}`)
			return
		}
		switch r.URL.Path {
		case "/healthz":
			_, _ = fmt.Fprintf(w, `{"status":"ok","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())
		case "/readyz":
			_, _ = fmt.Fprintf(w, `{"status":"ready","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	port := serverPort(t, srv.URL)
	if err := runfile.Write(cfg.runFile, runfile.Info{PID: 4242, Port: port, StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	out, _, err := executeCLI(t, Deps{
		ProcessAlive: func(pid int) bool { return pid == 4242 || pid == os.Getpid() },
		StartProcess: func(processStartConfig) error {
			info, err := runfile.Read(cfg.runFile)
			if err != nil {
				t.Fatal(err)
			}
			if info != nil {
				t.Fatalf("stale run-file was not removed before spawn: %#v", info)
			}
			spawned.Store(true)
			if err := runfile.Write(cfg.runFile, runfile.Info{PID: os.Getpid(), Port: port, StartedAt: time.Unix(110, 0).UTC()}); err != nil {
				t.Fatal(err)
			}
			return nil
		},
		Now: func() time.Time { return time.Unix(120, 0).UTC() },
	}, "start", "--json")
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(out, `"state": "ready"`) {
		t.Fatalf("start did not report ready after clearing stale run-file:\n%s", out)
	}
}

func TestStartEmitsCLIInvocationAfterReady(t *testing.T) {
	cfg := setConfigEnv(t)
	var spawned atomic.Bool
	called := make(chan string, 1)
	port := 3001
	out, _, err := executeCLI(t, Deps{
		HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
			switch req.URL.Path {
			case "/healthz":
				if !spawned.Load() {
					return jsonResponse(http.StatusOK, `{"status":"ok","service":"not-ao","pid":4242}`), nil
				}
				return jsonResponse(http.StatusOK, fmt.Sprintf(`{"status":"ok","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())), nil
			case "/readyz":
				if !spawned.Load() {
					return jsonResponse(http.StatusOK, `{"status":"not_ready","service":"not-ao","pid":4242}`), nil
				}
				return jsonResponse(http.StatusOK, fmt.Sprintf(`{"status":"ready","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())), nil
			case "/internal/telemetry/cli-invoked":
				called <- req.URL.Path
				return jsonResponse(http.StatusAccepted, ""), nil
			default:
				return jsonResponse(http.StatusNotFound, ""), nil
			}
		})},
		ProcessAlive: func(pid int) bool { return pid == os.Getpid() },
		StartProcess: func(processStartConfig) error {
			spawned.Store(true)
			return runfile.Write(cfg.runFile, runfile.Info{PID: os.Getpid(), Port: port, StartedAt: time.Unix(110, 0).UTC()})
		},
		Now: func() time.Time { return time.Unix(120, 0).UTC() },
	}, "start", "--json")
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(out, `"state": "ready"`) {
		t.Fatalf("start did not report ready:\n%s", out)
	}
	select {
	case path := <-called:
		if path != "/internal/telemetry/cli-invoked" {
			t.Fatalf("telemetry path = %q, want /internal/telemetry/cli-invoked", path)
		}
	default:
		t.Fatal("start did not emit CLI invocation after readiness")
	}
}

func TestStopRemovesStaleRunFile(t *testing.T) {
	cfg := setConfigEnv(t)
	if err := runfile.Write(cfg.runFile, runfile.Info{PID: 999999, Port: 3001, StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	out, _, err := executeCLI(t, Deps{ProcessAlive: func(int) bool { return false }}, "stop", "--json")
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(out, `"state": "stopped"`) {
		t.Fatalf("stop did not report stopped:\n%s", out)
	}
	info, err := runfile.Read(cfg.runFile)
	if err != nil {
		t.Fatal(err)
	}
	if info != nil {
		t.Fatalf("stale run-file was not removed: %#v", info)
	}
}

func TestStopDoesNotShutdownUnverifiedReusedPID(t *testing.T) {
	cfg := setConfigEnv(t)
	shutdownCalled := make(chan struct{}, 1)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/healthz":
			_, _ = w.Write([]byte(`{"status":"ok"}`))
		case "/readyz":
			_, _ = w.Write([]byte(`{"status":"ready"}`))
		case "/shutdown":
			shutdownCalled <- struct{}{}
			http.Error(w, "unexpected shutdown", http.StatusInternalServerError)
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	if err := runfile.Write(cfg.runFile, runfile.Info{PID: 4242, Port: serverPort(t, srv.URL), StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	out, _, err := executeCLI(t, Deps{
		ProcessAlive: func(pid int) bool { return pid == 4242 },
	}, "stop", "--json")
	if err != nil {
		t.Fatal(err)
	}
	select {
	case <-shutdownCalled:
		t.Fatal("stop requested shutdown from a process whose health probe did not prove AO daemon ownership")
	default:
	}
	if !strings.Contains(out, `"state": "stopped"`) {
		t.Fatalf("stop did not report stopped:\n%s", out)
	}
	info, err := runfile.Read(cfg.runFile)
	if err != nil {
		t.Fatal(err)
	}
	if info != nil {
		t.Fatalf("unverified run-file was not removed: %#v", info)
	}
}

func TestStopUsesShutdownEndpoint(t *testing.T) {
	cfg := setConfigEnv(t)
	shutdownCalled := make(chan struct{}, 1)
	var shutdownSeen atomic.Bool
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/healthz":
			_, _ = fmt.Fprintf(w, `{"status":"ok","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())
		case "/readyz":
			_, _ = fmt.Fprintf(w, `{"status":"ready","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())
		case "/shutdown":
			if err := runfile.Remove(cfg.runFile); err != nil {
				t.Fatal(err)
			}
			shutdownSeen.Store(true)
			shutdownCalled <- struct{}{}
			_, _ = fmt.Fprintf(w, `{"status":"shutting_down","service":%q,"pid":%d}`, daemonmeta.ServiceName, os.Getpid())
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	if err := runfile.Write(cfg.runFile, runfile.Info{PID: os.Getpid(), Port: serverPort(t, srv.URL), StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	out, _, err := executeCLI(t, Deps{
		ProcessAlive: func(pid int) bool {
			if pid != os.Getpid() {
				return false
			}
			return !shutdownSeen.Load()
		},
	}, "stop", "--json")
	if err != nil {
		t.Fatal(err)
	}
	select {
	case <-shutdownCalled:
	default:
		t.Fatal("stop did not call daemon shutdown endpoint")
	}
	if !strings.Contains(out, `"state": "stopped"`) {
		t.Fatalf("stop did not report stopped:\n%s", out)
	}
}

func TestStatusKeepsLiveProbeFailureUnhealthy(t *testing.T) {
	cfg := setConfigEnv(t)
	if err := runfile.Write(cfg.runFile, runfile.Info{PID: 4242, Port: closedPort(t), StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	out, _, err := executeCLI(t, Deps{
		ProcessAlive: func(pid int) bool { return pid == 4242 },
	}, "status", "--json")
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(out, `"state": "unhealthy"`) {
		t.Fatalf("status should keep live probe failures unhealthy:\n%s", out)
	}
	info, err := runfile.Read(cfg.runFile)
	if err != nil {
		t.Fatal(err)
	}
	if info == nil {
		t.Fatal("live probe failure should not remove run-file")
	}
}

func TestStopRefusesUnverifiedLivePID(t *testing.T) {
	cfg := setConfigEnv(t)
	if err := runfile.Write(cfg.runFile, runfile.Info{PID: 4242, Port: closedPort(t), StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	_, _, err := executeCLI(t, Deps{
		ProcessAlive: func(pid int) bool { return pid == 4242 },
	}, "stop", "--json")
	if err == nil {
		t.Fatal("stop should fail when daemon ownership cannot be verified")
	}
	info, err := runfile.Read(cfg.runFile)
	if err != nil {
		t.Fatal(err)
	}
	if info == nil {
		t.Fatal("unverified live PID should remain tracked")
	}
}

func TestStartDoesNotSpawnWhenLiveProbeFails(t *testing.T) {
	cfg := setConfigEnv(t)
	if err := runfile.Write(cfg.runFile, runfile.Info{PID: 4242, Port: closedPort(t), StartedAt: time.Unix(100, 0).UTC()}); err != nil {
		t.Fatal(err)
	}

	var started bool
	_, _, err := executeCLI(t, Deps{
		ProcessAlive: func(pid int) bool { return pid == 4242 },
		StartProcess: func(processStartConfig) error {
			started = true
			return nil
		},
	}, "start", "--timeout", "1ns", "--json")
	if err == nil {
		t.Fatal("start should fail instead of spawning over a live unverified PID")
	}
	if started {
		t.Fatal("start spawned while run-file PID was still alive")
	}
}

type testConfig struct {
	runFile string
	dataDir string
}

func setConfigEnv(t *testing.T) testConfig {
	t.Helper()
	dir := t.TempDir()
	cfg := testConfig{
		runFile: filepath.Join(dir, "running.json"),
		dataDir: filepath.Join(dir, "data"),
	}
	t.Setenv("AO_RUN_FILE", cfg.runFile)
	t.Setenv("AO_DATA_DIR", cfg.dataDir)
	t.Setenv("AO_PORT", "3001")
	t.Setenv("AO_REQUEST_TIMEOUT", "")
	t.Setenv("AO_SHUTDOWN_TIMEOUT", "")
	return cfg
}

func executeCLI(t *testing.T, deps Deps, args ...string) (string, string, error) {
	t.Helper()
	var out, errOut bytes.Buffer
	deps.Out = &out
	deps.Err = &errOut
	if deps.Sleep == nil {
		deps.Sleep = func(time.Duration) {}
	}
	cmd := NewRootCommand(deps)
	cmd.SetArgs(args)
	err := cmd.Execute()
	return out.String(), errOut.String(), err
}

func serverPort(t *testing.T, raw string) int {
	t.Helper()
	u, err := url.Parse(raw)
	if err != nil {
		t.Fatal(err)
	}
	_, portRaw, err := net.SplitHostPort(u.Host)
	if err != nil {
		t.Fatal(err)
	}
	port, err := strconv.Atoi(portRaw)
	if err != nil {
		t.Fatal(err)
	}
	return port
}

func closedPort(t *testing.T) int {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()

	_, portRaw, err := net.SplitHostPort(ln.Addr().String())
	if err != nil {
		t.Fatal(err)
	}
	port, err := strconv.Atoi(portRaw)
	if err != nil {
		t.Fatal(err)
	}
	return port
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }

func jsonResponse(status int, body string) *http.Response {
	if body == "" {
		body = "{}"
	}
	return &http.Response{
		StatusCode: status,
		Header:     make(http.Header),
		Body:       io.NopCloser(strings.NewReader(body)),
	}
}
