package cli_test

import (
	"context"
	"strings"
	"testing"
	"time"

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

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"echo": {
				Command: "sh",
				Args:    []string{"-c", `printf "reply:%s\n" "$1"`, "sh"},
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

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

	events := sink.Events()
	var types []string
	for _, e := range events {
		types = append(types, string(e.Type))
	}
	if len(events) < 3 {
		t.Fatalf("expected at least 3 events (start/delta/complete), got %v", types)
	}
	if events[0].Type != noderuntime.EventTypeStart {
		t.Fatalf("first event should be start, got %q", events[0].Type)
	}
	if events[len(events)-1].Type != noderuntime.EventTypeComplete {
		t.Fatalf("last event should 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 TestCLIExecuteOneShotAntigravityPrintProfilePassesPrompt(t *testing.T) {
	testutil.RequireUnixShell(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"antigravity": {
				Command: "sh",
				Args:    []string{"-c", `if [ "$1" = "--dangerously-skip-permissions" ] && [ "$2" = "--print" ]; then printf "reply:%s\n" "$3"; else printf "invalid args: %s %s %s\n" "$1" "$2" "$3"; fi`, "sh", "--dangerously-skip-permissions", "--print"},
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-agy",
		Target: "antigravity",
		Input:  map[string]any{"prompt": "hello-antigravity"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	events := sink.Events()
	if len(events) < 3 {
		t.Fatalf("expected at least 3 events, got %d", len(events))
	}
	if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hello-antigravity") {
		t.Fatalf("expected reply:hello-antigravity in deltas, got %q", combined)
	}
}

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

	script := `cat <<'EOF'
{"type":"init","timestamp":"2026-05-03T00:00:00.000Z","session_id":"abc"}
{"type":"message","role":"user","content":"hi"}
{"type":"message","role":"assistant","content":"Hello","delta":true}
{"type":"message","role":"assistant","content":" world","delta":true}
{"type":"result","status":"success"}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"streamy": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "stream-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-sj",
		Target: "streamy",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	combined := testutil.CollectDeltas(sink.Events())
	if combined != "Hello world" {
		t.Fatalf("expected stream-json deltas to concat to %q, got %q", "Hello world", combined)
	}
}

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

	script := `cat <<'EOF'
{"type":"thread.started","thread_id":"019d-aaa"}
{"type":"turn.started"}
{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Hi there."}}
{"type":"turn.completed","usage":{"input_tokens":10,"output_tokens":3}}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"codexy": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "codex-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-cj",
		Target: "codexy",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	combined := testutil.CollectDeltas(sink.Events())
	if combined != "Hi there." {
		t.Fatalf("expected codex-json deltas to be %q, got %q", "Hi there.", combined)
	}
}

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

	script := `cat <<'EOF'
{"type":"system","subtype":"init","session_id":"abc"}
{"type":"stream_event","event":{"type":"message_start","message":{"id":"msg_1"}}}
{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}}
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}}
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}}
{"type":"assistant","message":{"id":"msg_1","content":[{"type":"text","text":"Hello world"}]}}
{"type":"stream_event","event":{"type":"content_block_stop","index":0}}
{"type":"stream_event","event":{"type":"message_stop"}}
{"type":"result","subtype":"success","is_error":false,"result":"Hello world"}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"claudish": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "claude-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-cl",
		Target: "claudish",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	combined := testutil.CollectDeltas(sink.Events())
	if combined != "Hello world" {
		t.Fatalf("expected claude-json deltas to be %q, got %q", "Hello world", combined)
	}
}

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

	script := `cat <<'EOF'
{"type":"step_start","sessionID":"ses_1","part":{"type":"step-start"}}
{"type":"text","sessionID":"ses_1","part":{"type":"text","text":"Hi from opencode."}}
{"type":"step_finish","sessionID":"ses_1","part":{"type":"step-finish","reason":"stop"}}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"opencode": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "opencode-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-oc",
		Target: "opencode",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	combined := testutil.CollectDeltas(sink.Events())
	if combined != "Hi from opencode." {
		t.Fatalf("expected opencode-json deltas to be %q, got %q", "Hi from opencode.", combined)
	}
}

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

	script := `cat <<'EOF'
{"type":"step_start","sessionID":"ses_1","part":{"type":"step-start"}}
{"type":"error","sessionID":"ses_1","error":{"name":"UnknownError","data":{"message":"Model not found: definitely-invalid-provider/does-not-exist"}}}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"opencode": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "opencode-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-oc-err",
		Target: "opencode",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	if combined := testutil.CollectDeltas(sink.Events()); combined != "" {
		t.Fatalf("expected no deltas for error-only stream, got %q", combined)
	}
	want := "Model not found: definitely-invalid-provider/does-not-exist"
	var got string
	for _, e := range sink.Events() {
		if e.Type == noderuntime.EventTypeError {
			got = e.Error
			break
		}
	}
	if got != want {
		t.Fatalf("expected nested error message %q, got %q", want, got)
	}
}

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

	script := `cat <<'EOF'
{"type":"error","sessionID":"ses_1","error":{"name":"ProviderUnavailable"}}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"opencode": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "opencode-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-oc-err2",
		Target: "opencode",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	var got string
	for _, e := range sink.Events() {
		if e.Type == noderuntime.EventTypeError {
			got = e.Error
			break
		}
	}
	if got != "ProviderUnavailable" {
		t.Fatalf("expected fallback to error.name, got %q", got)
	}
}

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

	script := `cat <<'EOF'
not-json
{"type":"text","sessionID":"ses_1","part":{"type":"text","text":""}}
{"type":"text","sessionID":"ses_1","part":{"type":"text","text":"valid"}}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"opencode": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "opencode-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-oc2",
		Target: "opencode",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	combined := testutil.CollectDeltas(sink.Events())
	if combined != "valid" {
		t.Fatalf("expected only valid delta, got %q", combined)
	}
}

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

	script := `cat <<'EOF'
{"type":"task_started","taskId":"1777860858386"}
{"ts":1777860858391,"type":"say","say":"task","text":"Hi","modelInfo":{"providerId":"ollama","modelId":"qwen3.6:35b-a3b-bf16","mode":"act"}}
{"ts":1777860858965,"type":"say","say":"api_req_started","text":"{\"request\":\"<task>Hi</task>\"}"}
{"ts":1777860864009,"type":"say","say":"text","text":"Hello from Cline.","partial":false}
{"type":"completion","status":"success","timestamp":1777860865000}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"cline-dgx": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "cline-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-cline",
		Target: "cline-dgx",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	combined := testutil.CollectDeltas(sink.Events())
	if combined != "Hello from Cline." {
		t.Fatalf("expected cline-json deltas to be %q, got %q", "Hello from Cline.", combined)
	}
}

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

	script := `cat <<'EOF'
{"type":"task_started","taskId":"1777860858386"}
{"ts":1777860858965,"type":"ask","ask":"api_req_failed","text":"model unavailable"}
EOF`
	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"cline-dgx": {
				Command:      "sh",
				Args:         []string{"-c", script, "sh"},
				OutputFormat: "cline-json",
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-cline-err",
		Target: "cline-dgx",
		Input:  map[string]any{"prompt": "hi"},
	}, sink); err != nil {
		t.Fatalf("execute: %v", err)
	}
	var got string
	for _, e := range sink.Events() {
		if e.Type == noderuntime.EventTypeError {
			got = e.Error
			break
		}
	}
	if got != "model unavailable" {
		t.Fatalf("expected cline error message, got %q", got)
	}
}

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

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"noisy": {
				Command: "sh",
				Args: []string{
					"-c",
					`i=0; while [ "$i" -lt 20000 ]; do echo "warning line $i" >&2; i=$((i+1)); done; printf "reply:%s\n" "$1"`,
					"sh",
				},
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	err := c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:  "run-noisy",
		Target: "noisy",
		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 TestCLIExecuteOneShot_TimeoutEmitsTimeoutMessage(t *testing.T) {
	testutil.RequireUnixShell(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"slow": {
				Command: "sh",
				Args:    []string{"-c", `sleep 10`},
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

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

	err := c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:  "run-timeout",
		Target: "slow",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != noderuntime.ErrRunCancelled {
		t.Fatalf("expected ErrRunCancelled, got %v", err)
	}

	var lastMsg string
	var cancelEvent bool
	events := sink.Events()
	if len(events) > 0 {
		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 TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage(t *testing.T) {
	testutil.RequireUnixShell(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"slow-cancel": {
				Command: "sh",
				Args:    []string{"-c", `sleep 10`},
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		time.Sleep(100 * time.Millisecond)
		cancel()
	}()

	err := c.Execute(ctx, noderuntime.ExecutionSpec{
		RunID:  "run-usercancel",
		Target: "slow-cancel",
		Input:  map[string]any{"prompt": "hello"},
	}, sink)
	if err != noderuntime.ErrRunCancelled {
		t.Fatalf("expected ErrRunCancelled, got %v", err)
	}

	var lastMsg string
	var cancelEvent bool
	events := sink.Events()
	if len(events) > 0 {
		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 TestCLIExecuteOneShotStreamsStdoutChunks(t *testing.T) {
	testutil.RequireUnixShell(t)

	cfg := config.CLIConf{
		Enabled: true,
		Profiles: map[string]config.CLIProfileConf{
			"chunky": {
				Command: "sh",
				Args: []string{
					"-c",
					`printf "first"; sleep 0.2; printf "second"`,
					"sh",
				},
			},
		},
	}
	c := clipkg.New(cfg, zap.NewNop())
	sink := &testutil.FakeSink{}

	err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
		RunID:  "run-chunky",
		Target: "chunky",
		Input:  map[string]any{"prompt": "unused"},
	}, sink)
	if err != nil {
		t.Fatalf("execute: %v", err)
	}

	var deltas []string
	for _, event := range sink.Events() {
		if event.Type == noderuntime.EventTypeDelta {
			deltas = append(deltas, event.Delta)
		}
	}
	if len(deltas) < 2 {
		t.Fatalf("expected stdout chunks to be emitted separately, got %v", deltas)
	}
	if got := strings.Join(deltas, ""); got != "firstsecond" {
		t.Fatalf("combined deltas: got %q want firstsecond", got)
	}
}
