package node

import (
	"bytes"
	"context"
	"strings"
	"sync"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"

	runtime "iop/packages/go/execution"
	iop "iop/proto/gen/iop"
)

// mockSender is a minimal mock for protoSender.
type mockSender struct {
	sent       *iop.RunEvent
	sentEvents []*iop.RunEvent
}

func (m *mockSender) Send(msg proto.Message) error {
	if re, ok := msg.(*iop.RunEvent); ok {
		m.sent = re
		m.sentEvents = append(m.sentEvents, re)
	}
	return nil
}

func TestSessionSinkEmitIncludesNodeID(t *testing.T) {
	ms := &mockSender{}
	sink := &sessionSink{
		sess:       ms,
		nodeID:     "test-node-123",
		sessionID:  "session-abc",
		background: true,
	}

	event := runtime.RuntimeEvent{
		RunID:     "run-xyz",
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	}

	err := sink.Emit(context.Background(), event)
	if err != nil {
		t.Fatalf("Emit failed: %v", err)
	}

	if ms.sent == nil {
		t.Fatal("no event sent")
	}

	if ms.sent.NodeId != "test-node-123" {
		t.Errorf("expected NodeId %q, got %q", "test-node-123", ms.sent.NodeId)
	}
	if ms.sent.SessionId != "session-abc" {
		t.Errorf("expected SessionId %q, got %q", "session-abc", ms.sent.SessionId)
	}
	if !ms.sent.Background {
		t.Error("expected Background true")
	}
}

func TestSessionSinkPrintEventStreamsDeltaImmediately(t *testing.T) {
	ms := &mockSender{}
	var out bytes.Buffer
	sink := &sessionSink{
		sess:      ms,
		out:       &out,
		nodeID:    "test-node-123",
		sessionID: "session-abc",
	}

	start := runtime.RuntimeEvent{
		RunID:     "run-xyz",
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	}
	delta1 := runtime.RuntimeEvent{
		RunID:     "run-xyz",
		Type:      runtime.EventTypeDelta,
		Delta:     "Hello",
		Timestamp: time.Now(),
	}
	delta2 := runtime.RuntimeEvent{
		RunID:     "run-xyz",
		Type:      runtime.EventTypeDelta,
		Delta:     " world",
		Timestamp: time.Now(),
	}
	complete := runtime.RuntimeEvent{
		RunID:     "run-xyz",
		Type:      runtime.EventTypeComplete,
		Message:   "done",
		Timestamp: time.Now(),
	}

	if err := sink.Emit(context.Background(), start); err != nil {
		t.Fatalf("Emit start failed: %v", err)
	}
	if err := sink.Emit(context.Background(), delta1); err != nil {
		t.Fatalf("Emit delta1 failed: %v", err)
	}
	got := out.String()
	if !strings.Contains(got, "[node-message] Hello") {
		t.Fatalf("expected first delta to be printed immediately, got %q", got)
	}

	if err := sink.Emit(context.Background(), delta2); err != nil {
		t.Fatalf("Emit delta2 failed: %v", err)
	}
	got = out.String()
	if !strings.Contains(got, "[node-message] Hello world") {
		t.Fatalf("expected second delta to append to stream, got %q", got)
	}

	if err := sink.Emit(context.Background(), complete); err != nil {
		t.Fatalf("Emit complete failed: %v", err)
	}
	got = out.String()
	if !strings.Contains(got, "[node-event] complete run_id=run-xyz detail=\"done\"") {
		t.Fatalf("expected complete event to be printed, got %q", got)
	}
	if strings.Count(got, "[node-message]") != 1 {
		t.Fatalf("expected node-message prefix once, got %q", got)
	}
}

func TestTerminalDeferringSinkDropsPostTerminalEvents(t *testing.T) {
	ms := &mockSender{}
	var out bytes.Buffer
	inner := &sessionSink{
		sess:      ms,
		out:       &out,
		nodeID:    "test-node-123",
		sessionID: "session-abc",
	}
	sink := &terminalDeferringSink{inner: inner}

	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-terminal",
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit start failed: %v", err)
	}
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-terminal",
		Type:      runtime.EventTypeComplete,
		Message:   "done",
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit complete failed: %v", err)
	}
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-terminal",
		Type:      runtime.EventTypeDelta,
		Delta:     "late",
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit late delta failed: %v", err)
	}

	if len(ms.sentEvents) != 1 {
		t.Fatalf("expected only start to be sent before flush, got %d events", len(ms.sentEvents))
	}
	if got := ms.sentEvents[0].GetType(); got != string(runtime.EventTypeStart) {
		t.Fatalf("first event type = %q, want start", got)
	}
	if strings.Contains(out.String(), "complete run_id=run-terminal") {
		t.Fatalf("complete was printed before flush: %q", out.String())
	}

	if err := sink.Flush(context.Background()); err != nil {
		t.Fatalf("Flush failed: %v", err)
	}
	if len(ms.sentEvents) != 2 {
		t.Fatalf("expected start and complete after flush, got %d events", len(ms.sentEvents))
	}
	if got := ms.sentEvents[1].GetType(); got != string(runtime.EventTypeComplete) {
		t.Fatalf("second event type = %q, want complete", got)
	}
	if !strings.Contains(out.String(), "[node-event] complete run_id=run-terminal detail=\"done\"") {
		t.Fatalf("complete was not printed after flush: %q", out.String())
	}
	if strings.Contains(out.String(), "late") {
		t.Fatalf("late delta was printed after flush: %q", out.String())
	}
}

type blockingProtoSender struct {
	mu           sync.Mutex
	events       []*iop.RunEvent
	firstEntered chan struct{}
	releaseFirst chan struct{}
}

func (m *blockingProtoSender) Send(msg proto.Message) error {
	re, ok := msg.(*iop.RunEvent)
	if !ok {
		return nil
	}
	m.mu.Lock()
	first := len(m.events) == 0
	m.events = append(m.events, re)
	m.mu.Unlock()

	if first {
		close(m.firstEntered)
		<-m.releaseFirst
	}
	return nil
}

func (m *blockingProtoSender) sentTypes() []string {
	m.mu.Lock()
	defer m.mu.Unlock()
	types := make([]string, len(m.events))
	for i, e := range m.events {
		types[i] = e.GetType()
	}
	return types
}

func TestTerminalDeferringSinkPreservesConcurrentDeliveryOrder(t *testing.T) {
	ms := &blockingProtoSender{
		firstEntered: make(chan struct{}),
		releaseFirst: make(chan struct{}),
	}
	inner := &sessionSink{
		sess:      ms,
		nodeID:    "test-node",
		sessionID: "session-x",
	}
	sink := &terminalDeferringSink{inner: inner}

	startDone := make(chan error, 1)
	go func() {
		startDone <- sink.Emit(context.Background(), runtime.RuntimeEvent{
			RunID:     "run-conc",
			Type:      runtime.EventTypeStart,
			Timestamp: time.Now(),
		})
	}()

	<-ms.firstEntered // Non-terminal start holds emitMu inside inner.Emit and is blocked.

	// The terminal Emit begins while start still holds emitMu, so the two calls
	// overlap. emitMu forces complete to wait until start has been delivered to
	// the inner sink, so ordering does not depend on goroutine scheduling.
	completeDone := make(chan error, 1)
	go func() {
		completeDone <- sink.Emit(context.Background(), runtime.RuntimeEvent{
			RunID:     "run-conc",
			Type:      runtime.EventTypeComplete,
			Message:   "done",
			Timestamp: time.Now(),
		})
	}()

	close(ms.releaseFirst)

	// Wait for both Emit calls so the terminal is provably accepted and deferred
	// before Flush runs. This replaces the previous concurrent Flush, which could
	// win emitMu and drain an empty queue before complete was appended.
	if err := <-startDone; err != nil {
		t.Fatalf("Emit start failed: %v", err)
	}
	if err := <-completeDone; err != nil {
		t.Fatalf("Emit complete failed: %v", err)
	}

	// Terminal is deferred, not yet exposed to the inner sink: only start delivered.
	if pre := ms.sentTypes(); len(pre) != 1 || pre[0] != string(runtime.EventTypeStart) {
		t.Fatalf("expected only start before flush, got %v", pre)
	}

	if err := sink.Flush(context.Background()); err != nil {
		t.Fatalf("Flush failed: %v", err)
	}

	post := ms.sentTypes()
	if len(post) != 2 {
		t.Fatalf("expected 2 events, got %d %v", len(post), post)
	}
	if post[0] != string(runtime.EventTypeStart) {
		t.Fatalf("first event type = %q, want start", post[0])
	}
	if post[1] != string(runtime.EventTypeComplete) {
		t.Fatalf("second event type = %q, want complete", post[1])
	}
}

func TestTerminalDeferringSinkRecordsTerminal(t *testing.T) {
	ms := &mockSender{}
	inner := &sessionSink{
		sess:      ms,
		nodeID:    "test-node",
		sessionID: "session-x",
	}
	sink := &terminalDeferringSink{inner: inner}

	// Emit a complete terminal event.
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-1",
		Type:      runtime.EventTypeComplete,
		Message:   "ok",
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit complete: %v", err)
	}
	if !sink.hasTerminalObserved() {
		t.Fatal("expected terminalObserved to be true after emitting a terminal event")
	}

	// Emit a delta (should not change terminalObserved).
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-1",
		Type:      runtime.EventTypeDelta,
		Delta:     "x",
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit delta: %v", err)
	}
	if !sink.hasTerminalObserved() {
		t.Fatal("expected terminalObserved to remain true")
	}
}

func TestTerminalDeferringSinkNoTerminalNotMarked(t *testing.T) {
	ms := &mockSender{}
	inner := &sessionSink{
		sess:      ms,
		nodeID:    "test-node",
		sessionID: "session-x",
	}
	sink := &terminalDeferringSink{inner: inner}

	// Only emit start and delta (no terminal event).
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-2",
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit start: %v", err)
	}
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-2",
		Type:      runtime.EventTypeDelta,
		Delta:     "data",
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit delta: %v", err)
	}
	if sink.hasTerminalObserved() {
		t.Fatal("expected terminalObserved to be false when no terminal event emitted")
	}
}

func TestTerminalDeferringSinkDoesNotDuplicateAdapterTerminalEvent(t *testing.T) {
	ms := &mockSender{}
	inner := &sessionSink{
		sess:      ms,
		nodeID:    "test-node",
		sessionID: "session-x",
	}
	sink := &terminalDeferringSink{inner: inner}

	// Adapter emits start then complete terminal event.
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-3",
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit start: %v", err)
	}
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-3",
		Type:      runtime.EventTypeComplete,
		Message:   "adapter done",
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit complete: %v", err)
	}

	// Flush should send both events.
	if err := sink.Flush(context.Background()); err != nil {
		t.Fatalf("Flush: %v", err)
	}
	if len(ms.sentEvents) != 2 {
		t.Fatalf("expected 2 events (start + complete), got %d", len(ms.sentEvents))
	}
	if ms.sentEvents[1].GetType() != string(runtime.EventTypeComplete) {
		t.Fatalf("second event type = %q, want complete", ms.sentEvents[1].GetType())
	}
	// Verify the adapter's own terminal event message is preserved (no synthetic replacement).
	if ms.sentEvents[1].Message != "adapter done" {
		t.Fatalf("expected Message=\"adapter done\", got %q", ms.sentEvents[1].Message)
	}
}

func TestTerminalDeferringSinkSynthesizedTerminalNotDuplicated(t *testing.T) {
	ms := &mockSender{}
	inner := &sessionSink{
		sess:      ms,
		nodeID:    "test-node",
		sessionID: "session-x",
	}
	sink := &terminalDeferringSink{inner: inner}

	// Adapter emits start and delta, but NO terminal event.
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-4",
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit start: %v", err)
	}
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     "run-4",
		Type:      runtime.EventTypeDelta,
		Delta:     "data",
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit delta: %v", err)
	}

	// Verify no terminal was observed.
	if sink.hasTerminalObserved() {
		t.Fatal("expected no terminal observed")
	}

	// Synthesize a terminal event (simulating synthAndEmitTerminal).
	synthEvent := runtime.RuntimeEvent{
		RunID:     "run-4",
		Type:      runtime.EventTypeComplete,
		Message:   "adapter completed without terminal event",
		Timestamp: time.Now(),
	}
	if err := sink.Emit(context.Background(), synthEvent); err != nil {
		t.Fatalf("Emit synthesized complete: %v", err)
	}

	// Flush should send all events including the synthesized one.
	if err := sink.Flush(context.Background()); err != nil {
		t.Fatalf("Flush: %v", err)
	}
	if len(ms.sentEvents) != 3 {
		t.Fatalf("expected 3 events (start, delta, complete), got %d", len(ms.sentEvents))
	}
	if ms.sentEvents[2].GetType() != string(runtime.EventTypeComplete) {
		t.Fatalf("third event type = %q, want complete", ms.sentEvents[2].GetType())
	}
	// Verify the synthesized event's message.
	if ms.sentEvents[2].Message != "adapter completed without terminal event" {
		t.Fatalf("expected synthesized Message=\"adapter completed without terminal event\", got %q", ms.sentEvents[2].Message)
	}
	// terminalObserved should now be true.
	if !sink.hasTerminalObserved() {
		t.Fatal("expected terminalObserved to be true after flush")
	}
}

func TestSessionSinkPrintEventPreservesReasoningDelta(t *testing.T) {
	ms := &mockSender{}
	var out bytes.Buffer
	sink := &sessionSink{
		sess:      ms,
		out:       &out,
		nodeID:    "test-node",
		sessionID: "session-x",
	}

	runID := "run-reasoning"

	// Emit start.
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     runID,
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit start: %v", err)
	}

	// Emit reasoning delta with multiline content.
	reasoning := runtime.RuntimeEvent{
		RunID:     runID,
		Type:      runtime.EventTypeReasoningDelta,
		Delta:     "사용자가 인사했습니다.\n😀\n",
		Timestamp: time.Now(),
	}
	if err := sink.Emit(context.Background(), reasoning); err != nil {
		t.Fatalf("Emit reasoning_delta: %v", err)
	}

	// Emit complete.
	if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     runID,
		Type:      runtime.EventTypeComplete,
		Message:   "done",
		Timestamp: time.Now(),
	}); err != nil {
		t.Fatalf("Emit complete: %v", err)
	}

	got := out.String()

	// Reasoning delta should be preserved with [node-reasoning] prefix.
	if !strings.Contains(got, "[node-reasoning] 사용자가 인사했습니다.") {
		t.Fatalf("expected [node-reasoning] line 1, got %q", got)
	}
	if !strings.Contains(got, "[node-reasoning] 😀") {
		if !strings.Contains(got, "[node-reasoning] ") {
			t.Fatalf("expected [node-reasoning] prefix in stdout, got %q", got)
		}
	}

	// stdout should NOT contain the raw reasoning_delta fallback.
	if strings.Contains(got, "reasoning_delta run_id=") && strings.Contains(got, "detail=\"\"") {
		t.Fatalf("reasoning_delta should not fall through to default case, got %q", got)
	}

	// [node-message] should not appear for reasoning-only output.
	if strings.Contains(got, "[node-message]") {
		t.Fatalf("[node-message] should not appear for reasoning-only output, got %q", got)
	}

	// Verify proto event preserves reasoning_delta type and delta content.
	if len(ms.sentEvents) != 3 {
		t.Fatalf("expected 3 proto events, got %d", len(ms.sentEvents))
	}
	if ms.sentEvents[1].GetType() != "reasoning_delta" {
		t.Fatalf("proto event type = %q, want reasoning_delta", ms.sentEvents[1].GetType())
	}
	if ms.sentEvents[1].Delta != "사용자가 인사했습니다.\n😀\n" {
		t.Fatalf("proto event Delta = %q, expected multiline reasoning text", ms.sentEvents[1].Delta)
	}
}
