package cli_test

import (
	"context"
	"errors"
	"go.uber.org/zap"
	clipkg "iop/apps/node/internal/adapters/cli"
	"iop/apps/node/internal/adapters/cli/internal/testutil"
	noderuntime "iop/apps/node/internal/runtime"
	"iop/packages/go/config"
	"os"
	osexec "os/exec"
	"strings"
	"testing"
	"time"
)

func TestCLIStartPersistentTerminalAndExecuteWritesPrompt(t *testing.T) {
	testutil.RequirePTYSupport(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"echo-persistent": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 300,
				StartupIdleTimeoutMS:  50,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	sink := &testutil.FakeSink{}
	err := c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:  "run-1",
		Target: "echo-persistent",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	events := sink.Events()
	var started, completed bool
	for _, e := range events {
		switch e.Type {
		case noderuntime.EventTypeStart:
			started = true
		case noderuntime.EventTypeComplete:
			completed = true
		}
	}
	if !started {
		t.Fatal("expected start event")
	}
	if !completed {
		t.Fatal("expected complete event")
	}
	if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hello") {
		t.Fatalf("expected reply:hello in deltas, got %q", combined)
	}
}

func TestCLIExecutePersistentTerminalSendsCarriageReturn(t *testing.T) {
	testutil.RequirePTYSupport(t)
	if _, err := osexec.LookPath("stty"); err != nil {
		t.Skip("stty required")
	}

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"raw-tui": {
				Command:               os.Args[0],
				Args:                  []string{"-test.run=TestRawTUIHelperProcess", "--"},
				Env:                   []string{"IOP_RAW_TUI_HELPER=1"},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 100,
				// This scenario asserts the raw "\r" terminator, so the prompt must be
				// written only after the helper has entered raw mode. The readiness
				// marker anchors the drain to that point; the timeout must exceed the
				// re-exec'd helper's worst-case cold start (~700ms observed) so the
				// drain never settles before the marker arrives.
				StartupIdleTimeoutMS: 800,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	// Generous bound: the startup drain alone is 800ms (see profile) and process
	// spawn is slow and variable in constrained CI, so a 1s cap would race the run
	// to completion. This still asserts completion, not a timeout.
	execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(execCtx, noderuntime.ExecutionSpec{
		RunID:  "run-raw-tui",
		Target: "raw-tui",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	events := sink.Events()
	if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hello") {
		t.Fatalf("expected reply:hello in deltas, got %q", combined)
	}
	if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete {
		t.Fatalf("expected complete event, got %s", last.Type)
	}
}

func TestCLIExecutePersistentTerminalAcceptsClaudeBypassWarning(t *testing.T) {
	testutil.RequirePTYSupport(t)
	if _, err := osexec.LookPath("stty"); err != nil {
		t.Skip("stty required")
	}

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claude-warning": {
				Command:               os.Args[0],
				Args:                  []string{"-test.run=TestRawTUIHelperProcess", "--"},
				Env:                   []string{"IOP_CLAUDE_WARNING_HELPER=1"},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 100,
				// The startup drain must observe the bypass-warning screen (and
				// auto-accept it) before the prompt is sent; cover the re-exec'd
				// helper's worst-case cold start, which is larger under -race.
				StartupIdleTimeoutMS: 800,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(execCtx, noderuntime.ExecutionSpec{
		RunID:  "run-claude-warning",
		Target: "claude-warning",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	if combined := testutil.CollectDeltas(sink.Events()); !strings.Contains(combined, "reply:hello") {
		t.Fatalf("expected reply:hello in deltas, got %q", combined)
	}
}

func TestCLIExecutePersistentTerminalAcceptsClaudeWorkspaceTrustAndBypassWarning(t *testing.T) {
	testutil.RequirePTYSupport(t)
	if _, err := osexec.LookPath("stty"); err != nil {
		t.Skip("stty required")
	}

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claude-trust-warning": {
				Command:               os.Args[0],
				Args:                  []string{"-test.run=TestRawTUIHelperProcess", "--"},
				Env:                   []string{"IOP_CLAUDE_TRUST_WARNING_HELPER=1"},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 100,
				// The startup drain must observe the trust + bypass-warning screens
				// (and auto-accept them) before the prompt is sent; cover the
				// re-exec'd helper's worst-case cold start, which is larger under -race.
				StartupIdleTimeoutMS: 800,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(execCtx, noderuntime.ExecutionSpec{
		RunID:  "run-claude-trust-warning",
		Target: "claude-trust-warning",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	if combined := testutil.CollectDeltas(sink.Events()); !strings.Contains(combined, "reply:hello") {
		t.Fatalf("expected reply:hello in deltas, got %q", combined)
	}
}

func TestCLIExecutePersistentClaudeTUICancelsSessionLimitUpgradePrompt(t *testing.T) {
	testutil.RequirePTYSupport(t)
	if _, err := osexec.LookPath("stty"); err != nil {
		t.Skip("stty required")
	}

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claude-tui": {
				Command:               os.Args[0],
				Args:                  []string{"-test.run=TestRawTUIHelperProcess", "--"},
				Env:                   []string{"IOP_CLAUDE_RATE_LIMIT_HELPER=1"},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 500,
				// Cover the re-exec'd helper's variable cold start so the startup
				// drain waits for its readiness marker before the prompt is sent.
				StartupIdleTimeoutMS: 300,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(execCtx, noderuntime.ExecutionSpec{
		RunID:  "run-claude-rate-limit",
		Target: "claude-tui",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if !errors.Is(err, noderuntime.ErrRunCancelled) {
		t.Fatalf("expected run cancelled, got %v", err)
	}
	var cancelMsg string
	for _, ev := range sink.Events() {
		if ev.Type == noderuntime.EventTypeCancelled {
			cancelMsg = ev.Message
		}
		if ev.Type == noderuntime.EventTypeError {
			t.Fatalf("expected cancellation, got error event %q", ev.Error)
		}
	}
	if !strings.Contains(cancelMsg, "session limit") {
		t.Fatalf("expected cancelled event to mention session limit, got %q", cancelMsg)
	}
}

func TestCLIExecutePersistentClaudeTUIReplaysPromptAfterStartupReadyRace(t *testing.T) {
	testutil.RequirePTYSupport(t)
	if _, err := osexec.LookPath("stty"); err != nil {
		t.Skip("stty required")
	}

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claude-tui": {
				Command:               os.Args[0],
				Args:                  []string{"-test.run=TestRawTUIHelperProcess", "--"},
				Env:                   []string{"IOP_CLAUDE_READY_REPLAY_HELPER=1"},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 100,
				// Cover the re-exec'd helper's variable cold start so the startup
				// drain observes the trust screen and auto-accepts it before the
				// prompt is sent, keeping the replay-after-ready path deterministic.
				StartupIdleTimeoutMS: 300,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(execCtx, noderuntime.ExecutionSpec{
		RunID:  "run-claude-ready-race",
		Target: "claude-tui",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}
	if combined := testutil.CollectDeltas(sink.Events()); combined != "replay:hello" {
		t.Fatalf("expected replayed prompt response, got %q", combined)
	}
}

func TestCLIStartPersistentTerminalEmitsRawChunksWithoutNewline(t *testing.T) {
	testutil.RequirePTYSupport(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"raw-terminal": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf "chunk:%s" "$line"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 100,
				StartupIdleTimeoutMS:  50,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	runCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(runCtx, noderuntime.ExecutionSpec{
		RunID:  "run-raw",
		Target: "raw-terminal",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	events := sink.Events()
	if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "chunk:hello") {
		t.Fatalf("expected raw chunk in deltas, got %q", combined)
	}
	if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete {
		t.Fatalf("expected complete event, got %s", last.Type)
	}
}

func TestCLIStartPersistentTerminalUsesWidePTY(t *testing.T) {
	testutil.RequirePTYSupport(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"size-terminal": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do stty size; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 100,
				StartupIdleTimeoutMS:  50,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	runCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(runCtx, noderuntime.ExecutionSpec{
		RunID:  "run-size",
		Target: "size-terminal",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	if combined := testutil.CollectDeltas(sink.Events()); !strings.Contains(combined, "720 1024") {
		t.Fatalf("expected wide PTY size in deltas, got %q", combined)
	}
}

func TestCLIExecutePersistentClaudeTUIFiltersTerminalChrome(t *testing.T) {
	testutil.RequirePTYSupport(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claude-tui": {
				Command: "sh",
				// The frame begins with cursor-home + erase-display, mirroring a real
				// claude TUI redraw, so the rendered assistant screen is independent
				// of whether the prompt raced `stty -echo` and got echoed: any echoed
				// characters are erased before the assistant line is drawn.
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf '\033[H\033[2J\033[2C\r\033[7A\342\227\217\033[1Cmessage:%s\r\n\302\267 Cogitating... (1s, 1 tokens)\r\n\342\235\257 ' "$line"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 100,
				StartupIdleTimeoutMS:  50,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	runCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(runCtx, noderuntime.ExecutionSpec{
		RunID:  "run-filter",
		Target: "claude-tui",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	events := sink.Events()
	combined := testutil.CollectDeltas(events)
	if combined != "message:hello" {
		t.Fatalf("expected only assistant message delta, got %q", combined)
	}
	if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete || last.Message != "prompt-ready" {
		t.Fatalf("expected prompt-ready completion, got type=%q message=%q", last.Type, last.Message)
	}
}

func TestCLIExecutePersistentClaudeTUIStreamsLineBeforeIdle(t *testing.T) {
	testutil.RequirePTYSupport(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claude-tui": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf 'sud fragment\r\n\342\234\242 Dilly-dallying\342\200\246\r\n* Dilly-dallying\342\200\246\r\n'; sleep 0.05; printf '\342\227\217 sudo runs commands\r\n\342\235\257 '; sleep 0.15; printf '\r\033[2A\033[2K\342\227\217 sudo runs commands with elevated privileges.\r\n\342\235\257 '; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 300,
				StartupIdleTimeoutMS:  50,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	runCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(runCtx, noderuntime.ExecutionSpec{
		RunID:  "run-growing",
		Target: "claude-tui",
		Input:  map[string]any{"prompt": "explain sudo"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	events := sink.Events()
	combined := testutil.CollectDeltas(events)
	if combined != "sudo runs commands with elevated privileges." {
		t.Fatalf("expected full growing message, got %q", combined)
	}
	if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete || last.Message != "prompt-ready" {
		t.Fatalf("expected prompt-ready completion, got type=%q message=%q", last.Type, last.Message)
	}
}

func TestCLIExecutePersistentClaudeTUIWaitsForPromptReadyAfterPartialOutput(t *testing.T) {
	testutil.RequirePTYSupport(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claude-tui": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf '\342\227\217 first chunk\r\n\342\234\242 Dilly-dallying\342\200\246\r\n'; sleep 0.15; printf 'second chunk\r\n\342\235\257 '; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 60,
				StartupIdleTimeoutMS:  50,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	runCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(runCtx, noderuntime.ExecutionSpec{
		RunID:  "run-delayed-tail",
		Target: "claude-tui",
		Input:  map[string]any{"prompt": "write two chunks"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	events := sink.Events()
	combined := testutil.CollectDeltas(events)
	if !strings.Contains(combined, "first chunk") ||
		!strings.Contains(combined, "second chunk") ||
		strings.Index(combined, "first chunk") > strings.Index(combined, "second chunk") {
		t.Fatalf("expected full delayed message, got %q", combined)
	}
	if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete || last.Message != "prompt-ready" {
		t.Fatalf("expected prompt-ready completion, got type=%q message=%q", last.Type, last.Message)
	}
}

func TestCLIExecutePersistentClaudeTUIDoesNotCompleteOnStatusOnlyIdle(t *testing.T) {
	testutil.RequirePTYSupport(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claude-tui": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf '3\r\n\r\nThundering\342\200\2466\r\n'; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 60,
				StartupIdleTimeoutMS:  50,
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())

	ctx := context.Background()
	if err := c.Start(ctx); err != nil {
		t.Fatalf("start: %v", err)
	}
	defer func() { _ = c.Stop(ctx) }()

	runCtx, cancel := context.WithTimeout(ctx, 220*time.Millisecond)
	defer cancel()

	sink := &testutil.FakeSink{}
	err := c.Execute(runCtx, noderuntime.ExecutionSpec{
		RunID:  "run-status-only",
		Target: "claude-tui",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err == nil {
		t.Fatal("execute completed successfully before any assistant message")
	}

	for _, event := range sink.Events() {
		if event.Type == noderuntime.EventTypeDelta {
			t.Fatalf("status-only output should not be emitted as delta: %q", event.Delta)
		}
		if event.Type == noderuntime.EventTypeComplete {
			t.Fatalf("status-only output should not emit complete: %+v", event)
		}
	}
}
