package bridge

import (
	"context"
	"encoding/json"
	"io"
	"log/slog"
	"os/exec"
	"strings"
	"testing"
	"time"
)

func TestNewBridge(t *testing.T) {
	b := NewCLIBridge("/usr/bin/agent", false, 4, 30*time.Second)
	if b == nil {
		t.Fatal("NewCLIBridge returned nil")
	}
	if b.cursorPath != "/usr/bin/agent" {
		t.Errorf("cursorPath = %q, want %q", b.cursorPath, "/usr/bin/agent")
	}
	if cap(b.semaphore) != 4 {
		t.Errorf("semaphore capacity = %d, want 4", cap(b.semaphore))
	}
	if b.timeout != 30*time.Second {
		t.Errorf("timeout = %v, want 30s", b.timeout)
	}
}

func TestNewBridge_DefaultConcurrency(t *testing.T) {
	b := NewCLIBridge("agent", false, 0, 10*time.Second)
	if cap(b.semaphore) != 1 {
		t.Errorf("semaphore capacity = %d, want 1 (default)", cap(b.semaphore))
	}
}

func TestNewBridge_NegativeConcurrency(t *testing.T) {
	b := NewCLIBridge("agent", false, -5, 10*time.Second)
	if cap(b.semaphore) != 1 {
		t.Errorf("semaphore capacity = %d, want 1 (default for negative)", cap(b.semaphore))
	}
}

func TestNewBridge_UsesACPWhenRequested(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	b := NewBridge("agent", logger, true, false, 2, 10*time.Second)
	if _, ok := b.(*ACPBridge); !ok {
		t.Fatalf("expected ACPBridge, got %T", b)
	}
}

func TestBuildACPCommandArgs_NoModel(t *testing.T) {
	got := buildACPCommandArgs("/tmp/workspace", "auto")
	want := []string{"--workspace", "/tmp/workspace", "acp"}
	if len(got) != len(want) {
		t.Fatalf("len(args) = %d, want %d (%v)", len(got), len(want), got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("args[%d] = %q, want %q (all=%v)", i, got[i], want[i], got)
		}
	}
}

func TestBuildACPCommandArgs_WithModel(t *testing.T) {
	got := buildACPCommandArgs("/tmp/workspace", "sonnet-4.6")
	want := []string{"--workspace", "/tmp/workspace", "--model", "sonnet-4.6", "acp"}
	if len(got) != len(want) {
		t.Fatalf("len(args) = %d, want %d (%v)", len(got), len(want), got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("args[%d] = %q, want %q (all=%v)", i, got[i], want[i], got)
		}
	}
}

func TestBuildCLICommandArgs_UsesAskMode(t *testing.T) {
	got := buildCLICommandArgs("hello", "auto", "/tmp/workspace", true, false)
	wantPrefix := []string{
		"--print",
		"--mode", "ask",
		"--workspace", "/tmp/workspace",
		"--model", "auto",
		"--stream-partial-output", "--output-format", "stream-json",
	}
	if len(got) != len(wantPrefix)+1 {
		t.Fatalf("unexpected arg length: %v", got)
	}
	for i := range wantPrefix {
		if got[i] != wantPrefix[i] {
			t.Fatalf("args[%d] = %q, want %q (all=%v)", i, got[i], wantPrefix[i], got)
		}
	}
	if got[len(got)-1] != "hello" {
		t.Fatalf("last arg = %q, want prompt", got[len(got)-1])
	}
}

func TestBuildCLICommandArgs_ChatOnlyAddsTrust(t *testing.T) {
	got := buildCLICommandArgs("hi", "", "/tmp/ws", false, true)
	found := false
	for _, a := range got {
		if a == "--trust" {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("expected --trust when chatOnly=true, got args: %v", got)
	}
}

// mockCmdHelper builds a bridge that executes a fake command for channel logic testing.
func mockCmdBridge(t *testing.T) *CLIBridge {
	t.Helper()
	// Use "echo" as a mock command that outputs valid JSON lines
	// We'll override Execute logic by using a custom cursorPath that is "echo"
	return NewCLIBridge("echo", false, 2, 5*time.Second)
}

func TestExecute_ContextCancelled(t *testing.T) {
	b := NewCLIBridge("/bin/sleep", false, 1, 30*time.Second)
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cancel immediately

	outputChan, errChan := b.Execute(ctx, "test prompt", "gpt-4", "")

	// Should receive error due to cancelled context
	select {
	case err := <-errChan:
		if err == nil {
			t.Error("expected error from cancelled context, got nil")
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for error from cancelled context")
	}

	// outputChan should be closed
	select {
	case _, ok := <-outputChan:
		if ok {
			t.Error("expected outputChan to be closed")
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for outputChan to close")
	}
}

func TestExecute_SemaphoreBlocking(t *testing.T) {
	b := NewCLIBridge("/bin/sleep", false, 1, 30*time.Second)

	// Fill the semaphore
	b.semaphore <- struct{}{}

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

	_, errChan := b.Execute(ctx, "test", "model", "")

	// Should get error because semaphore is full and context times out
	select {
	case err := <-errChan:
		if err == nil {
			t.Error("expected error, got nil")
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for semaphore blocking error")
	}

	// Release the semaphore
	<-b.semaphore
}

func TestExecute_InvalidCommand(t *testing.T) {
	b := NewCLIBridge("/nonexistent/command", false, 1, 5*time.Second)
	ctx := context.Background()

	outputChan, errChan := b.Execute(ctx, "test", "model", "")

	var outputs []string
	for line := range outputChan {
		outputs = append(outputs, line)
	}

	// Should have error from starting invalid command
	select {
	case err := <-errChan:
		if err == nil {
			t.Error("expected error for invalid command, got nil")
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for error")
	}
}

func TestExecute_ValidJSONOutput(t *testing.T) {
	// Use "printf" to simulate JSON line output
	b := NewCLIBridge("printf", false, 2, 5*time.Second)
	ctx := context.Background()

	// printf with JSON lines
	outputChan, errChan := b.Execute(ctx, `{"type":"assistant","message":{"content":[{"text":"hello"}]}}\n{"type":"result"}`, "model", "")

	var outputs []string
	for line := range outputChan {
		outputs = append(outputs, line)
	}

	// Check errChan for any errors
	select {
	case err := <-errChan:
		if err != nil {
			t.Logf("error (may be expected): %v", err)
		}
	default:
	}

	if len(outputs) == 0 {
		t.Log("no outputs received (printf may not handle newlines as expected)")
	} else {
		t.Logf("received %d output lines", len(outputs))
	}
}

func TestHandleACPNotification_ForwardsAgentMessageChunk(t *testing.T) {
	w := &acpWorker{}
	var got strings.Builder
	w.setActiveSinkLocked(func(text string) {
		got.WriteString(text)
	})

	params, err := json.Marshal(map[string]interface{}{
		"sessionId": "s1",
		"update": map[string]interface{}{
			"sessionUpdate": "agent_message_chunk",
			"content": map[string]interface{}{
				"type": "text",
				"text": "嗨",
			},
		},
	})
	if err != nil {
		t.Fatalf("marshal params: %v", err)
	}

	handled := w.handleACPNotification(acpMessage{
		Method: "session/update",
		Params: params,
	})

	if !handled {
		t.Fatal("expected session/update to be handled")
	}
	if got.String() != "嗨" {
		t.Fatalf("sink text = %q, want %q", got.String(), "嗨")
	}
}

func TestHandleACPNotification_IgnoresNonAssistantContentUpdate(t *testing.T) {
	w := &acpWorker{}
	var got strings.Builder
	w.setActiveSinkLocked(func(text string) {
		got.WriteString(text)
	})

	params, err := json.Marshal(map[string]interface{}{
		"sessionId": "s1",
		"update": map[string]interface{}{
			"sessionUpdate": "planner_thought_chunk",
			"content": map[string]interface{}{
				"type": "text",
				"text": "Handling user greetings",
			},
		},
	})
	if err != nil {
		t.Fatalf("marshal params: %v", err)
	}

	handled := w.handleACPNotification(acpMessage{
		Method: "session/update",
		Params: params,
	})

	if !handled {
		t.Fatal("expected session/update to be handled")
	}
	if got.String() != "" {
		t.Fatalf("sink text = %q, want empty", got.String())
	}
}

func TestReadLoop_DoesNotPanicWhenReaderDoneIsNil(t *testing.T) {
	w := &acpWorker{
		pending: make(map[int]chan acpResponse),
	}

	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("readLoop should not panic when readerDone is nil, got %v", r)
		}
	}()

	w.readLoop(io.NopCloser(strings.NewReader("")))
}

// Ensure exec is used (imported but may appear unused without integration tests)
var _ = exec.Command
