package cli

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"iop/apps/node/internal/runtime"
	"testing"
)

// TestCodexAppServerEventMap_Delta verifies AgentMessageDeltaNotification shape:
// { threadId, turnId, delta } → EventTypeDelta with source/thread_id/turn_id metadata.
func TestCodexAppServerEventMap_Delta(t *testing.T) {
	notif := appServerNotification{
		Method: "item/agentMessage/delta",
		Params: map[string]any{
			"threadId": "th-1",
			"turnId":   "tn-1",
			"delta":    "hello world",
		},
	}
	events, done, err := decodeAppServerNotification(notif, "run-1", "th-ctx", "tn-ctx")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if done {
		t.Fatal("delta should not signal done")
	}
	if len(events) != 1 {
		t.Fatalf("expected 1 event, got %d", len(events))
	}
	ev := events[0]
	if ev.Type != runtime.EventTypeDelta {
		t.Errorf("Type = %q, want %q", ev.Type, runtime.EventTypeDelta)
	}
	if ev.Delta != "hello world" {
		t.Errorf("Delta = %q, want %q", ev.Delta, "hello world")
	}
	if ev.Metadata["source"] != "codex-app-server" {
		t.Errorf("source metadata = %q", ev.Metadata["source"])
	}
	// params threadId/turnId should override execution-context values
	if ev.Metadata["thread_id"] != "th-1" {
		t.Errorf("thread_id metadata = %q, want %q", ev.Metadata["thread_id"], "th-1")
	}
	if ev.Metadata["turn_id"] != "tn-1" {
		t.Errorf("turn_id metadata = %q, want %q", ev.Metadata["turn_id"], "tn-1")
	}
}

// TestCodexAppServerEventMap_DeltaMetadata verifies fallback to execution-context ids
// when notification params do not carry threadId/turnId.
func TestCodexAppServerEventMap_DeltaMetadata(t *testing.T) {
	notif := appServerNotification{
		Method: "item/agentMessage/delta",
		Params: map[string]any{"delta": "chunk"},
	}
	events, _, err := decodeAppServerNotification(notif, "run-x", "th-fallback", "tn-fallback")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(events) != 1 {
		t.Fatalf("expected 1 event, got %d", len(events))
	}
	if events[0].Metadata["thread_id"] != "th-fallback" {
		t.Errorf("thread_id = %q, want %q", events[0].Metadata["thread_id"], "th-fallback")
	}
	if events[0].Metadata["turn_id"] != "tn-fallback" {
		t.Errorf("turn_id = %q, want %q", events[0].Metadata["turn_id"], "tn-fallback")
	}
}

// TestCodexAppServerEventMap_TurnCompleted verifies that turn/completed with status
// "completed" maps to EventTypeComplete and signals done.
func TestCodexAppServerEventMap_TurnCompleted(t *testing.T) {
	notif := appServerNotification{
		Method: "turn/completed",
		Params: map[string]any{
			"threadId": "th-2",
			"turn":     map[string]any{"id": "tn-2", "status": "completed"},
		},
	}
	events, done, err := decodeAppServerNotification(notif, "run-2", "", "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !done {
		t.Fatal("turn/completed should signal done")
	}
	if len(events) != 1 || events[0].Type != runtime.EventTypeComplete {
		t.Fatalf("expected 1 complete event, got %+v", events)
	}
	if events[0].Metadata["thread_id"] != "th-2" {
		t.Errorf("thread_id = %q, want %q", events[0].Metadata["thread_id"], "th-2")
	}
}

// TestCodexAppServerEventMap_TurnCompletedFailed verifies that turn/completed with
// status "failed" maps to EventTypeError (not complete).
func TestCodexAppServerEventMap_TurnCompletedFailed(t *testing.T) {
	notif := appServerNotification{
		Method: "turn/completed",
		Params: map[string]any{
			"threadId": "th-3",
			"turn": map[string]any{
				"id":     "tn-3",
				"status": "failed",
				"error":  map[string]any{"message": "context window exceeded"},
			},
		},
	}
	events, done, err := decodeAppServerNotification(notif, "run-3", "", "")
	if err == nil {
		t.Fatal("expected non-nil error for failed turn")
	}
	if done {
		t.Fatal("failed turn should not signal done as complete")
	}
	if len(events) != 1 || events[0].Type != runtime.EventTypeError {
		t.Fatalf("expected 1 error event, got %+v", events)
	}
	if events[0].Error != "context window exceeded" {
		t.Errorf("Error = %q, want %q", events[0].Error, "context window exceeded")
	}
}

// TestCodexAppServerEventMap_Error verifies ErrorNotification shape:
// { error: { message }, threadId, turnId } → EventTypeError with full metadata.
func TestCodexAppServerEventMap_Error(t *testing.T) {
	notif := appServerNotification{
		Method: "error",
		Params: map[string]any{
			"error":    map[string]any{"message": "quota exceeded"},
			"threadId": "th-4",
			"turnId":   "tn-4",
		},
	}
	events, _, err := decodeAppServerNotification(notif, "run-4", "", "")
	if err == nil {
		t.Fatal("expected non-nil error for error notification")
	}
	if len(events) != 1 || events[0].Type != runtime.EventTypeError {
		t.Fatalf("expected 1 error event, got %+v", events)
	}
	if events[0].Error != "quota exceeded" {
		t.Errorf("Error = %q, want %q", events[0].Error, "quota exceeded")
	}
	if events[0].Metadata["thread_id"] != "th-4" {
		t.Errorf("thread_id = %q, want %q", events[0].Metadata["thread_id"], "th-4")
	}
	if events[0].Metadata["turn_id"] != "tn-4" {
		t.Errorf("turn_id = %q, want %q", events[0].Metadata["turn_id"], "tn-4")
	}
}

// TestCodexAppServerEventMap_TurnFailed verifies that turn/failed (legacy) maps to EventTypeError.
func TestCodexAppServerEventMap_TurnFailed(t *testing.T) {
	notif := appServerNotification{
		Method: "turn/failed",
		Params: map[string]any{"error": map[string]any{"message": "rate limit"}},
	}
	events, _, err := decodeAppServerNotification(notif, "run-5", "", "")
	if err == nil {
		t.Fatal("expected non-nil error for turn/failed")
	}
	if len(events) != 1 || events[0].Type != runtime.EventTypeError {
		t.Fatalf("expected 1 error event, got %+v", events)
	}
	if events[0].Error != "rate limit" {
		t.Errorf("Error = %q, want %q", events[0].Error, "rate limit")
	}
}

// TestCodexAppServerDrainNotifications_DoesNotDropBurst verifies that a burst of
// more than notifCh capacity (32) deltas followed by a terminal turn/completed are
// all delivered without dropping the terminal event.
func TestCodexAppServerDrainNotifications_DoesNotDropBurst(t *testing.T) {
	const burstSize = 40 // > default channel capacity of 32

	stdinR, stdinW := io.Pipe()
	stdoutR, stdoutW := io.Pipe()
	_ = stdinR

	proc := &codexAppServerProc{
		stdin:   stdinW,
		stdout:  bufio.NewScanner(stdoutR),
		notifCh: make(chan appServerNotification, 32),
		done:    make(chan struct{}),
	}
	proc.stdout.Buffer(make([]byte, 64*1024), 4*1024*1024)
	go proc.readLoop()

	// Write burstSize delta notifications then a turn/completed.
	enc := json.NewEncoder(stdoutW)
	go func() {
		for i := 0; i < burstSize; i++ {
			_ = enc.Encode(map[string]any{
				"method": "item/agentMessage/delta",
				"params": map[string]any{"delta": fmt.Sprintf("chunk-%d", i)},
			})
		}
		_ = enc.Encode(map[string]any{
			"method": "turn/completed",
			"params": map[string]any{"turn": map[string]any{"status": "completed"}},
		})
		_ = stdoutW.Close()
	}()

	sink := &testSink{}
	err := codexAppServerDrainNotifications(context.Background(), proc, "run-burst", "th-burst", "tn-burst", sink)
	_ = stdinW.Close()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	var deltas, completes int
	for _, ev := range sink.events {
		switch ev.Type {
		case runtime.EventTypeDelta:
			deltas++
		case runtime.EventTypeComplete:
			completes++
		}
	}
	if deltas != burstSize {
		t.Errorf("delta count = %d, want %d", deltas, burstSize)
	}
	if completes != 1 {
		t.Errorf("complete count = %d, want 1", completes)
	}
}

// TestCodexAppServerSend_ResponsePriorityOverDone verifies that send() returns
// the response (not a process-exited error) when the fake server sends a
// response and then closes stdout simultaneously.
func TestCodexAppServerSend_ResponsePriorityOverDone(t *testing.T) {
	stdinR, stdinW := io.Pipe()
	stdoutR, stdoutW := io.Pipe()

	// Fake server: read one request, respond, then close stdout (simulate exit).
	// stdinR is owned exclusively by this goroutine — no concurrent reader.
	go func() {
		defer stdinR.Close()
		dec := json.NewDecoder(stdinR)
		enc := json.NewEncoder(stdoutW)
		var req map[string]json.RawMessage
		if err := dec.Decode(&req); err != nil {
			_ = stdoutW.Close()
			return
		}
		var id int64
		_ = json.Unmarshal(req["id"], &id)
		_ = enc.Encode(map[string]any{"id": id, "result": map[string]any{"ok": true}})
		_ = stdoutW.Close()
	}()

	proc := &codexAppServerProc{
		stdin:   stdinW,
		stdout:  bufio.NewScanner(stdoutR),
		notifCh: make(chan appServerNotification, 8),
		done:    make(chan struct{}),
	}
	proc.stdout.Buffer(make([]byte, 64*1024), 4*1024*1024)
	go proc.readLoop()

	resp, err := proc.send(context.Background(), "ping", nil)
	_ = stdinW.Close()
	if err != nil {
		t.Fatalf("send() returned error instead of response: %v", err)
	}
	if resp.Result == nil {
		t.Fatal("send() returned nil result")
	}
}
