package core

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"testing"
	"time"
)

// makeFakeClaude crea un script bash temporal que escribe las lineas dadas a
// stdout y termina con el exit code indicado.
func makeFakeClaude(t *testing.T, lines []string, exitCode int) string {
	t.Helper()

	// Skip si bash no esta disponible
	if _, err := exec.LookPath("bash"); err != nil {
		t.Skip("bash not available, skipping claude_stream tests")
	}

	dir := t.TempDir()
	script := filepath.Join(dir, "claude")

	var sb string
	sb = "#!/usr/bin/env bash\n"
	for _, l := range lines {
		// Escapar comillas simples para echo
		escaped := ""
		for _, ch := range l {
			if ch == '\'' {
				escaped += "'\\''"
			} else {
				escaped += string(ch)
			}
		}
		sb += fmt.Sprintf("printf '%%s\\n' '%s'\n", escaped)
	}
	if exitCode != 0 {
		sb += fmt.Sprintf("exit %d\n", exitCode)
	}

	if err := os.WriteFile(script, []byte(sb), 0o755); err != nil {
		t.Fatalf("write fake claude: %v", err)
	}
	return script
}

// collectEvents drena el canal con timeout.
func collectEvents(t *testing.T, ch <-chan ClaudeEvent, timeout time.Duration) []ClaudeEvent {
	t.Helper()
	var events []ClaudeEvent
	deadline := time.After(timeout)
	for {
		select {
		case ev, ok := <-ch:
			if !ok {
				return events
			}
			events = append(events, ev)
		case <-deadline:
			t.Fatal("timeout waiting for events channel to close")
		}
	}
}

func TestStreamClaude_SystemEvent(t *testing.T) {
	line := `{"type":"system","subtype":"init","session_id":"abc123","model":"claude-sonnet-4-5"}`
	bin := makeFakeClaude(t, []string{line}, 0)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var found *ClaudeEvent
	for i := range events {
		if events[i].Type == ClaudeEventSystem {
			found = &events[i]
			break
		}
	}
	if found == nil {
		t.Fatalf("no system event found; got %v", events)
	}
	if found.SessionID != "abc123" {
		t.Errorf("session_id: got %q, want %q", found.SessionID, "abc123")
	}
	if found.Model != "claude-sonnet-4-5" {
		t.Errorf("model: got %q, want %q", found.Model, "claude-sonnet-4-5")
	}
	if found.Subtype != "init" {
		t.Errorf("subtype: got %q, want %q", found.Subtype, "init")
	}
}

func TestStreamClaude_TextDelta(t *testing.T) {
	line := `{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hola mundo"}]}}`
	bin := makeFakeClaude(t, []string{line}, 0)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var found *ClaudeEvent
	for i := range events {
		if events[i].Type == ClaudeEventTextDelta {
			found = &events[i]
			break
		}
	}
	if found == nil {
		t.Fatalf("no text_delta event found; got %v", events)
	}
	if found.Text != "hola mundo" {
		t.Errorf("text: got %q, want %q", found.Text, "hola mundo")
	}
}

func TestStreamClaude_MultipleTextBlocks(t *testing.T) {
	// Un solo mensaje assistant con dos bloques text
	line := `{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"primero"},{"type":"text","text":"segundo"}]}}`
	bin := makeFakeClaude(t, []string{line}, 0)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var deltas []ClaudeEvent
	for _, ev := range events {
		if ev.Type == ClaudeEventTextDelta {
			deltas = append(deltas, ev)
		}
	}
	if len(deltas) != 2 {
		t.Fatalf("expected 2 text_delta events, got %d: %v", len(deltas), deltas)
	}
	if deltas[0].Text != "primero" {
		t.Errorf("delta[0].text: got %q, want %q", deltas[0].Text, "primero")
	}
	if deltas[1].Text != "segundo" {
		t.Errorf("delta[1].text: got %q, want %q", deltas[1].Text, "segundo")
	}
}

func TestStreamClaude_ToolUse(t *testing.T) {
	inputJSON := `{"column_id":"col1","title":"nueva card"}`
	line := fmt.Sprintf(`{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_abc","name":"create_card","input":%s}]}}`, inputJSON)
	bin := makeFakeClaude(t, []string{line}, 0)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var found *ClaudeEvent
	for i := range events {
		if events[i].Type == ClaudeEventToolUse {
			found = &events[i]
			break
		}
	}
	if found == nil {
		t.Fatalf("no tool_use event found; got %v", events)
	}
	if found.ToolUseID != "toolu_abc" {
		t.Errorf("tool_use_id: got %q, want %q", found.ToolUseID, "toolu_abc")
	}
	if found.ToolName != "create_card" {
		t.Errorf("tool_name: got %q, want %q", found.ToolName, "create_card")
	}
	var input map[string]string
	if err := json.Unmarshal(found.ToolInput, &input); err != nil {
		t.Fatalf("unmarshal tool_input: %v", err)
	}
	if input["title"] != "nueva card" {
		t.Errorf("tool_input.title: got %q, want %q", input["title"], "nueva card")
	}
}

func TestStreamClaude_ToolResultStringContent(t *testing.T) {
	line := `{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_abc","content":"card creada exitosamente","is_error":false}]}}`
	bin := makeFakeClaude(t, []string{line}, 0)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var found *ClaudeEvent
	for i := range events {
		if events[i].Type == ClaudeEventToolResult {
			found = &events[i]
			break
		}
	}
	if found == nil {
		t.Fatalf("no tool_result event found; got %v", events)
	}
	if found.ToolResultID != "toolu_abc" {
		t.Errorf("tool_result_id: got %q, want %q", found.ToolResultID, "toolu_abc")
	}
	if found.ToolResultContent != "card creada exitosamente" {
		t.Errorf("content: got %q, want %q", found.ToolResultContent, "card creada exitosamente")
	}
	if found.ToolResultIsError {
		t.Error("is_error should be false")
	}
}

func TestStreamClaude_ToolResultArrayContent(t *testing.T) {
	line := `{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_xyz","content":[{"type":"text","text":"parte a"},{"type":"text","text":"parte b"}],"is_error":false}]}}`
	bin := makeFakeClaude(t, []string{line}, 0)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var found *ClaudeEvent
	for i := range events {
		if events[i].Type == ClaudeEventToolResult {
			found = &events[i]
			break
		}
	}
	if found == nil {
		t.Fatalf("no tool_result event found; got %v", events)
	}
	if found.ToolResultContent != "parte aparte b" {
		t.Errorf("content: got %q, want %q", found.ToolResultContent, "parte aparte b")
	}
}

func TestStreamClaude_ResultEvent(t *testing.T) {
	line := `{"type":"result","subtype":"success","is_error":false,"result":"respuesta final","stop_reason":"end_turn","session_id":"sess1"}`
	bin := makeFakeClaude(t, []string{line}, 0)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var found *ClaudeEvent
	for i := range events {
		if events[i].Type == ClaudeEventResult {
			found = &events[i]
			break
		}
	}
	if found == nil {
		t.Fatalf("no result event found; got %v", events)
	}
	if found.StopReason != "end_turn" {
		t.Errorf("stop_reason: got %q, want %q", found.StopReason, "end_turn")
	}
	if found.IsError {
		t.Error("is_error should be false")
	}
	if found.Result != "respuesta final" {
		t.Errorf("result: got %q, want %q", found.Result, "respuesta final")
	}
}

func TestStreamClaude_NonZeroExit(t *testing.T) {
	bin := makeFakeClaude(t, nil, 7)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var found *ClaudeEvent
	for i := range events {
		if events[i].Type == ClaudeEventError {
			found = &events[i]
			break
		}
	}
	if found == nil {
		t.Fatalf("no error event for non-zero exit; got %v", events)
	}
	if found.Error == "" {
		t.Error("error message should not be empty")
	}
}

func TestStreamClaude_NonJsonLine(t *testing.T) {
	bin := makeFakeClaude(t, []string{"esto no es json"}, 0)

	ch, err := StreamClaude(context.Background(), ClaudeStreamOpts{Bin: bin})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}
	events := collectEvents(t, ch, 5*time.Second)

	var found *ClaudeEvent
	for i := range events {
		if events[i].Type == ClaudeEventError {
			found = &events[i]
			break
		}
	}
	if found == nil {
		t.Fatalf("no error event for non-json line; got %v", events)
	}
}

func TestStreamClaude_CtxCancel(t *testing.T) {
	// Fake claude que duerme indefinidamente
	if _, err := exec.LookPath("bash"); err != nil {
		t.Skip("bash not available")
	}
	dir := t.TempDir()
	script := filepath.Join(dir, "claude")
	if err := os.WriteFile(script, []byte("#!/usr/bin/env bash\nsleep 60\n"), 0o755); err != nil {
		t.Fatalf("write sleep script: %v", err)
	}

	ctx, cancel := context.WithCancel(context.Background())

	ch, err := StreamClaude(ctx, ClaudeStreamOpts{Bin: script})
	if err != nil {
		t.Fatalf("StreamClaude error: %v", err)
	}

	// Cancelar enseguida
	cancel()

	// El canal debe cerrarse en menos de 1 segundo
	deadline := time.After(1 * time.Second)
	for {
		select {
		case _, ok := <-ch:
			if !ok {
				return // canal cerrado: test OK
			}
		case <-deadline:
			t.Fatal("channel did not close within 1s after ctx cancel")
		}
	}
}
