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"
)

func TestCLIExecutePersistentWaitsForSlowFirstOutput(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 0.2; 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-1",
		Target: "slow-echo",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

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

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"exit-on-input": {
				Command:               "sh",
				Args:                  []string{"-c", `stty -echo; while IFS= read -r line; do printf "before-exit\n"; exit 2; 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) }()

	sink := &testutil.FakeSink{}
	err := c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:  "run-1",
		Target: "exit-on-input",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err == nil {
		t.Fatal("expected non-nil error when persistent process exits")
	}

	errStr := err.Error()
	t.Logf("Got error: %q", errStr)
	if !strings.Contains(errStr, "before-exit") {
		t.Errorf("expected error to contain recent output 'before-exit', got %q", errStr)
	}
	if !strings.Contains(errStr, "exit status 2") {
		t.Errorf("expected error to contain exit status 'exit status 2', got %q", errStr)
	}
	if !strings.Contains(errStr, "target=exit-on-input") {
		t.Errorf("expected error to contain target=exit-on-input, got %q", errStr)
	}

	events := sink.Events()
	var errorEvent noderuntime.RuntimeEvent
	var hasError bool
	for _, e := range events {
		if e.Type == noderuntime.EventTypeError {
			hasError = true
			errorEvent = e
		}
	}
	if !hasError {
		t.Fatal("expected error event in emitted events")
	}
	if !strings.Contains(errorEvent.Error, "before-exit") {
		t.Errorf("expected error event to contain recent output, got %q", errorEvent.Error)
	}
	if len(events) > 0 && events[len(events)-1].Type == noderuntime.EventTypeComplete {
		t.Fatal("last event should not be complete when process exits unexpectedly")
	}
}

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

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

	sink1 := &testutil.FakeSink{}
	err := c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:  "run-1",
		Target: "consecutive-echo",
		Input:  map[string]any{"prompt": "first"},
	}, sink1)
	if err != nil {
		t.Fatalf("first execute: %v", err)
	}
	events1 := sink1.Events()
	if events1[len(events1)-1].Type != noderuntime.EventTypeComplete {
		t.Fatalf("first run: expected last event to be complete, got %q", events1[len(events1)-1].Type)
	}

	sink2 := &testutil.FakeSink{}
	err = c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:  "run-2",
		Target: "consecutive-echo",
		Input:  map[string]any{"prompt": "second"},
	}, sink2)
	if err != nil {
		t.Fatalf("second execute: %v", err)
	}
	events2 := sink2.Events()
	if events2[len(events2)-1].Type != noderuntime.EventTypeComplete {
		t.Fatalf("second run: expected last event to be complete, got %q", events2[len(events2)-1].Type)
	}

	for _, e := range events2 {
		if e.Type == noderuntime.EventTypeDelta && strings.Contains(e.Delta, "first") {
			t.Fatalf("second run received first run's delta (session contamination): %q", e.Delta)
		}
	}
}

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

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

	sinkA := &testutil.FakeSink{}
	err := c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:     "run-a",
		Target:    "multi-echo",
		SessionID: "session-a",
		Input:     map[string]any{"prompt": "alpha"},
	}, sinkA)
	if err != nil {
		t.Fatalf("session-a execute: %v", err)
	}

	sinkB := &testutil.FakeSink{}
	err = c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:     "run-b",
		Target:    "multi-echo",
		SessionID: "session-b",
		Input:     map[string]any{"prompt": "beta"},
	}, sinkB)
	if err != nil {
		t.Fatalf("session-b execute: %v", err)
	}

	if combined := testutil.CollectDeltas(sinkA.Events()); !strings.Contains(combined, "alpha") {
		t.Fatalf("session-a: expected alpha in deltas, got %q", combined)
	}
	if combined := testutil.CollectDeltas(sinkB.Events()); !strings.Contains(combined, "beta") {
		t.Fatalf("session-b: expected beta in deltas, got %q", combined)
	}
	if combined := testutil.CollectDeltas(sinkA.Events()); strings.Contains(combined, "beta") {
		t.Fatalf("session-a contaminated with session-b output: %q", combined)
	}
}

func TestCLIExecutePersistentRequireExistingSessionFailsWhenMissing(t *testing.T) {
	testutil.RequireUnixShell(t)

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

	sink := &testutil.FakeSink{}
	err := c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:       "run-1",
		Target:      "echo-req",
		SessionID:   "nonexistent",
		SessionMode: noderuntime.SessionModeRequireExisting,
		Input:       map[string]any{"prompt": "hello"},
	}, sink)
	if err == nil {
		t.Fatal("expected error for require-existing with missing session")
	}
	if strings.Contains(err.Error(), "start") {
		t.Fatalf("should not have started a new process, got: %v", err)
	}
}

func TestCLIExecutePersistentMaintainsHundredLogicalSessions(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping heavy session test in short mode")
	}
	testutil.RequireUnixShell(t)

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

	const n = 20
	for i := 0; i < n; i++ {
		sessionID := "session-" + string(rune('a'+i%26)) + "-" + string(rune('0'+i/26))
		sink := &testutil.FakeSink{}
		err := c.Execute(ctx, noderuntime.ExecutionSpec{
			RunID:     "run-first-" + sessionID,
			Target:    "hundred-echo",
			SessionID: sessionID,
			Input:     map[string]any{"prompt": "ping-" + sessionID},
		}, sink)
		if err != nil {
			t.Fatalf("session %q first execute: %v", sessionID, err)
		}
	}

	// Second round — all sessions must still be alive.
	for i := 0; i < n; i++ {
		sessionID := "session-" + string(rune('a'+i%26)) + "-" + string(rune('0'+i/26))
		sink := &testutil.FakeSink{}
		err := c.Execute(ctx, noderuntime.ExecutionSpec{
			RunID:     "run-second-" + sessionID,
			Target:    "hundred-echo",
			SessionID: sessionID,
			Input:     map[string]any{"prompt": "pong-" + sessionID},
		}, sink)
		if err != nil {
			t.Fatalf("session %q second execute: %v", sessionID, err)
		}
	}

	_ = c.Stop(ctx)
}
