package provider

import (
	"context"
	"encoding/json"
	"strings"
	"testing"

	"github.com/smallnest/pigo/internal/agentcore"
)

// A recorded Anthropic Messages API SSE stream covering a text block, a
// thinking block (with signature), and a tool_use block, ending with a
// tool_use stop reason and output-token usage. Trimmed but structurally
// faithful to the real wire format.
const anthropicToolUseSSE = `event: message_start
data: {"type":"message_start","message":{"id":"msg_01ABC","model":"claude-opus-4-8","usage":{"input_tokens":42,"output_tokens":1}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Let me think. "}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Use the tool."}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"sigABC"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"I'll check "}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"the weather."}}

event: content_block_stop
data: {"type":"content_block_stop","index":1}

event: content_block_start
data: {"type":"content_block_start","index":2,"content_block":{"type":"tool_use","id":"toolu_01","name":"get_weather"}}

event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"city\":"}}

event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":" \"SF\"}"}}

event: content_block_stop
data: {"type":"content_block_stop","index":2}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":57}}

event: message_stop
data: {"type":"message_stop"}

`

// feedSSE splits a recorded SSE body into events (blank-line separated),
// extracts each `data:` payload, and drives the decoder exactly as the
// transport pump would, returning all emitted events plus the final message.
func feedSSE(t *testing.T, dec Decoder, body string) ([]StreamEvent, agentcore.AssistantMessage) {
	t.Helper()
	var events []StreamEvent
	for _, block := range strings.Split(body, "\n\n") {
		var payload strings.Builder
		for _, line := range strings.Split(block, "\n") {
			line = strings.TrimRight(line, "\r")
			if strings.HasPrefix(line, "data:") {
				payload.WriteString(strings.TrimSpace(strings.TrimPrefix(line, "data:")))
			}
		}
		if payload.Len() == 0 {
			continue
		}
		// [DONE] is a transport-level terminator, not a decoder payload.
		if payload.String() == "[DONE]" {
			continue
		}
		evs, err := dec.Decode([]byte(payload.String()))
		if err != nil {
			t.Fatalf("decode %q: %v", payload.String(), err)
		}
		events = append(events, evs...)
	}
	finalEvents, err := dec.Finish()
	if err != nil {
		t.Fatalf("finish: %v", err)
	}
	events = append(events, finalEvents...)

	var final agentcore.AssistantMessage
	for _, ev := range events {
		if d, ok := ev.(StreamDoneEvent); ok {
			final = d.Message
		}
	}
	return events, final
}

func TestAnthropicDecoderToolUseStream(t *testing.T) {
	dec := NewAnthropicDecoder()
	events, final := feedSSE(t, dec, anthropicToolUseSSE)

	// The first emitted event must be a start event.
	if len(events) == 0 || events[0].EventKind() != StreamEventStart {
		t.Fatalf("expected a start event first, got %v", eventKinds(events))
	}
	// The last emitted event must be the terminal done event.
	if events[len(events)-1].EventKind() != StreamEventDone {
		t.Fatalf("expected a done event last, got %v", eventKinds(events))
	}

	// Stop reason: tool_use.
	if final.StopReason != agentcore.StopReasonToolUse {
		t.Errorf("stop reason = %q, want tool_use", final.StopReason)
	}
	// Usage: input from message_start, output from message_delta.
	if final.Usage == nil || final.Usage.InputTokens != 42 || final.Usage.OutputTokens != 57 {
		t.Errorf("usage = %+v, want input=42 output=57", final.Usage)
	}
	// Response identity from message_start.
	if final.ResponseID != "msg_01ABC" || final.ResponseModel != "claude-opus-4-8" {
		t.Errorf("response id/model = %q/%q", final.ResponseID, final.ResponseModel)
	}

	// Content blocks in index order: thinking, text, tool_use.
	if len(final.Content) != 3 {
		t.Fatalf("expected 3 content blocks, got %d: %+v", len(final.Content), final.Content)
	}
	th, ok := final.Content[0].(agentcore.ThinkingContent)
	if !ok || th.Thinking != "Let me think. Use the tool." {
		t.Errorf("thinking block = %+v", final.Content[0])
	}
	if th.ThinkingSignature != "sigABC" {
		t.Errorf("thinking signature = %q, want sigABC", th.ThinkingSignature)
	}
	txt, ok := final.Content[1].(agentcore.TextContent)
	if !ok || txt.Text != "I'll check the weather." {
		t.Errorf("text block = %+v", final.Content[1])
	}
	tool, ok := final.Content[2].(agentcore.ToolCallContent)
	if !ok || tool.Name != "get_weather" || tool.ID != "toolu_01" {
		t.Fatalf("tool block = %+v", final.Content[2])
	}
	// tool_use JSON must have accumulated into valid arguments.
	var args map[string]string
	if err := json.Unmarshal(tool.Arguments, &args); err != nil {
		t.Fatalf("tool arguments not valid JSON %q: %v", tool.Arguments, err)
	}
	if args["city"] != "SF" {
		t.Errorf("tool arguments = %v, want city=SF", args)
	}
}

func TestAnthropicDecoderTextOnlyEndTurn(t *testing.T) {
	body := `data: {"type":"message_start","message":{"id":"msg_1","model":"claude-x","usage":{"input_tokens":10,"output_tokens":0}}}

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}

data: {"type":"content_block_stop","index":0}

data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":3}}

data: {"type":"message_stop"}

`
	dec := NewAnthropicDecoder()
	_, final := feedSSE(t, dec, body)
	if final.StopReason != agentcore.StopReasonEndTurn {
		t.Errorf("stop reason = %q, want end_turn", final.StopReason)
	}
	if len(final.Content) != 1 {
		t.Fatalf("expected 1 content block, got %d", len(final.Content))
	}
	txt, ok := final.Content[0].(agentcore.TextContent)
	if !ok || txt.Text != "Hello world" {
		t.Errorf("text = %+v", final.Content[0])
	}
}

func TestAnthropicDecoderMaxTokensMapsToLength(t *testing.T) {
	body := `data: {"type":"message_start","message":{"id":"m","model":"c","usage":{"input_tokens":5,"output_tokens":0}}}

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"truncated"}}

data: {"type":"content_block_stop","index":0}

data: {"type":"message_delta","delta":{"stop_reason":"max_tokens"},"usage":{"output_tokens":100}}

data: {"type":"message_stop"}

`
	dec := NewAnthropicDecoder()
	_, final := feedSSE(t, dec, body)
	if final.StopReason != agentcore.StopReasonLength {
		t.Errorf("max_tokens must map to length, got %q", final.StopReason)
	}
}

// TestAnthropicDecoderErrorEvent verifies an Anthropic `error` event becomes a
// decode error (which the transport turns into a terminal error event), never
// a panic.
func TestAnthropicDecoderErrorEvent(t *testing.T) {
	dec := NewAnthropicDecoder()
	_, err := dec.Decode([]byte(`{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}`))
	if err == nil {
		t.Fatal("error event must return a decode error")
	}
	if !strings.Contains(err.Error(), "overloaded_error") {
		t.Errorf("error should name the type, got %v", err)
	}
}

// TestAnthropicDecoderMalformedPayload verifies invalid JSON is a returned
// error (rides the stream as terminal error), not a panic.
func TestAnthropicDecoderMalformedPayload(t *testing.T) {
	dec := NewAnthropicDecoder()
	if _, err := dec.Decode([]byte(`{not json`)); err == nil {
		t.Fatal("malformed payload must return an error")
	}
}

// TestAnthropicDecoderFinishFlushesPartial verifies a stream cut short (no
// message_stop) still yields a done event on Finish so the partial isn't lost.
func TestAnthropicDecoderFinishFlushesPartial(t *testing.T) {
	body := `data: {"type":"message_start","message":{"id":"m","model":"c","usage":{"input_tokens":5,"output_tokens":0}}}

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"partial"}}

`
	dec := NewAnthropicDecoder()
	events, final := feedSSE(t, dec, body)
	if events[len(events)-1].EventKind() != StreamEventDone {
		t.Fatalf("Finish must emit a terminal done event, got %v", eventKinds(events))
	}
	// No message_delta arrived → default end_turn.
	if final.StopReason != agentcore.StopReasonEndTurn {
		t.Errorf("cut-short stream should default to end_turn, got %q", final.StopReason)
	}
	if len(final.Content) != 1 {
		t.Fatalf("expected the partial text block, got %+v", final.Content)
	}
}

// TestAnthropicDecoderThroughTransport wires the decoder through the real
// transport pump against a recorded SSE server, exercising the full path.
func TestAnthropicDecoderThroughTransport(t *testing.T) {
	srv := sseServer(t, anthropicToolUseSSE)
	defer srv.Close()

	stream, err := StreamRequest(context.Background(), TransportConfig{
		NewRequest: newReqFn(srv.URL),
		Decoder:    NewAnthropicDecoder(),
	})
	if err != nil {
		t.Fatalf("StreamRequest: %v", err)
	}
	var kinds []string
	for ev := range stream.Events() {
		kinds = append(kinds, ev.EventKind())
	}
	final, resErr := stream.Result(context.Background())
	if resErr != nil {
		t.Fatalf("result: %v", resErr)
	}
	if final.StopReason != agentcore.StopReasonToolUse {
		t.Errorf("stop reason via transport = %q, want tool_use", final.StopReason)
	}
	if len(final.Content) != 3 {
		t.Errorf("expected 3 content blocks via transport, got %d", len(final.Content))
	}
	if kinds[len(kinds)-1] != StreamEventDone {
		t.Errorf("stream must end with done, got %v", kinds)
	}
}

func eventKinds(events []StreamEvent) []string {
	out := make([]string, len(events))
	for i, ev := range events {
		out[i] = ev.EventKind()
	}
	return out
}
