package cli_test

import (
	"context"
	"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"
	"strings"
	"testing"
	"time"
)

// TestCLIExecutePersistentCancelDoesNotTerminateSession verifies that a
// cancelled run leaves the session alive so subsequent runs can reuse it.
func TestCLIExecutePersistentCancelDoesNotTerminateSession(t *testing.T) {
	testutil.RequirePTYSupport(t)

	// ResponseIdleTimeoutMS is short so the cancel-drain exits quickly,
	// allowing the second execute to acquire the session lock promptly.
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"slow-echo": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do sleep 0.05; printf "reply:%s\n" "$line"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 150,
				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) }()

	// First execute: cancel before idle timeout fires.
	firstCtx, firstCancel := context.WithTimeout(ctx, 100*time.Millisecond)
	defer firstCancel()

	sink1 := &testutil.FakeSink{}
	_ = c.Execute(firstCtx, noderuntime.ExecutionSpec{
		RunID:  "run-1",
		Target: "slow-echo",
		Input:  map[string]any{"prompt": "first"},
	}, sink1)

	// Give time for cancel drain to finish (drain uses idleTimeout=150ms).
	time.Sleep(400 * time.Millisecond)

	// Second execute on same session — must succeed (session still alive).
	execCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()

	sink2 := &testutil.FakeSink{}
	err := c.Execute(execCtx, noderuntime.ExecutionSpec{
		RunID:  "run-2",
		Target: "slow-echo",
		Input:  map[string]any{"prompt": "second"},
	}, sink2)
	if err != nil {
		t.Fatalf("second execute after cancel must succeed (session should be alive): %v", err)
	}
	events2 := sink2.Events()
	if len(events2) == 0 || events2[len(events2)-1].Type != noderuntime.EventTypeComplete {
		t.Fatalf("expected complete event on second run, got %v", events2)
	}
}

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"slow-echo": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do sleep 10; printf "reply:%s\n" "$line"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 500,
				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) }()

	execCtx, cancel := context.WithCancel(ctx)

	sink := &testutil.FakeSink{}
	go func() {
		time.Sleep(100 * time.Millisecond)
		cancel()
	}()

	err := c.Execute(execCtx, noderuntime.ExecutionSpec{
		RunID:  "run-usercancel",
		Target: "slow-echo",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err == nil {
		t.Fatalf("expected error, got nil")
	}

	events := sink.Events()
	var lastMsg string
	var cancelEvent bool
	for i := len(events) - 1; i >= 0; i-- {
		if events[i].Type == noderuntime.EventTypeCancelled {
			cancelEvent = true
			lastMsg = events[i].Message
		}
	}
	if !cancelEvent {
		t.Fatal("expected cancelled event")
	}
	if lastMsg != "user-cancel" {
		t.Fatalf("expected cancel Message 'user-cancel', got %q", lastMsg)
	}
}

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"slow-echo": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do sleep 10; printf "reply:%s\n" "$line"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 500,
				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) }()

	execCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
	defer cancel()

	sink := &testutil.FakeSink{}
	executeErr := c.Execute(execCtx, noderuntime.ExecutionSpec{
		RunID:  "run-timeout",
		Target: "slow-echo",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if executeErr == nil {
		t.Fatalf("expected error, got nil")
	}

	events := sink.Events()
	var lastMsg string
	var cancelEvent bool
	for i := len(events) - 1; i >= 0; i-- {
		if events[i].Type == noderuntime.EventTypeCancelled {
			cancelEvent = true
			lastMsg = events[i].Message
		}
	}
	if !cancelEvent {
		t.Fatal("expected cancelled event")
	}
	if lastMsg != "timeout" {
		t.Fatalf("expected cancel Message 'timeout', got %q", lastMsg)
	}
}

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"marker-line": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf "reply:%s\n" "$line"; printf "<<END_OF_RESPONSE>>\n"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 500,
				StartupIdleTimeoutMS:  50,
				CompletionMarker:      config.CompletionMarkerConf{Line: "<<END_OF_RESPONSE>>"},
			},
		},
	}
	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-marker-line",
		Target: "marker-line",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

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

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"marker-regex": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf "reply:%s\n" "$line"; printf "DONE 42\n"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 500,
				StartupIdleTimeoutMS:  50,
				CompletionMarker:      config.CompletionMarkerConf{Regex: "^DONE \\d+$"},
			},
		},
	}
	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-marker-regex",
		Target: "marker-regex",
		Input:  map[string]any{"prompt": "hi"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

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

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"no-marker": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do sleep 0.1; printf "reply:%s\n" "$line"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 50,
				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) }()

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

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

	events := sink.Events()
	last := events[len(events)-1]
	if last.Type != noderuntime.EventTypeComplete {
		t.Fatalf("expected last event to be complete, got %q", last.Type)
	}
	if last.Message != "idle-timeout" {
		t.Fatalf("expected Message 'idle-timeout' when no marker is set, got %q", last.Message)
	}
	if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:fallback") {
		t.Fatalf("expected reply:fallback in deltas, got %q", combined)
	}
}

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"idle-reason": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do sleep 0.1; printf "reply:%s\n" "$line"; done`},
				Persistent:            true,
				Terminal:              true,
				ResponseIdleTimeoutMS: 50,
				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) }()

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

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

	events := sink.Events()
	var complete *noderuntime.RuntimeEvent
	for i := range events {
		if events[i].Type == noderuntime.EventTypeComplete {
			complete = &events[i]
		}
	}
	if complete == nil {
		t.Fatal("expected complete event")
	}
	if complete.Message != "idle-timeout" {
		t.Fatalf("expected Message 'idle-timeout', got %q", complete.Message)
	}
}
