package effects

import (
	"context"
	"log/slog"
	"testing"

	"github.com/enmanuel/agents/pkg/decision"
)

// fakeSender records calls for assertions.
type fakeSender struct {
	calls []senderCall
}

type senderCall struct {
	method       string
	roomID       string
	threadRootID string
	inReplyTo    string
	markdown     string
}

func (f *fakeSender) SendText(ctx context.Context, roomID, text string) error {
	f.calls = append(f.calls, senderCall{method: "SendText", roomID: roomID, markdown: text})
	return nil
}

func (f *fakeSender) SendMarkdown(ctx context.Context, roomID, markdown string) error {
	f.calls = append(f.calls, senderCall{method: "SendMarkdown", roomID: roomID, markdown: markdown})
	return nil
}

func (f *fakeSender) SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error {
	f.calls = append(f.calls, senderCall{method: "SendReplyMarkdown", roomID: roomID, inReplyTo: inReplyTo, markdown: markdown})
	return nil
}

func (f *fakeSender) SendThreadMarkdown(ctx context.Context, roomID, threadRootID, inReplyTo, markdown string) error {
	f.calls = append(f.calls, senderCall{method: "SendThreadMarkdown", roomID: roomID, threadRootID: threadRootID, inReplyTo: inReplyTo, markdown: markdown})
	return nil
}

func (f *fakeSender) SendTyping(ctx context.Context, roomID string, typing bool) error {
	return nil
}

func TestExecuteReply_PlainMarkdown(t *testing.T) {
	sender := &fakeSender{}
	runner := NewRunner(sender, nil, slog.Default())

	actions := []decision.Action{{
		Kind:  decision.ActionKindReply,
		Reply: &decision.ReplyAction{Content: "hello"},
	}}

	results := runner.Execute(context.Background(), "!room:test", actions)
	if len(results) != 1 || results[0].Err != nil {
		t.Fatalf("unexpected results: %+v", results)
	}
	if len(sender.calls) != 1 {
		t.Fatalf("expected 1 call, got %d", len(sender.calls))
	}
	c := sender.calls[0]
	if c.method != "SendMarkdown" {
		t.Errorf("expected SendMarkdown, got %s", c.method)
	}
	if c.roomID != "!room:test" {
		t.Errorf("expected room !room:test, got %s", c.roomID)
	}
}

func TestExecuteReply_WithInReplyTo(t *testing.T) {
	sender := &fakeSender{}
	runner := NewRunner(sender, nil, slog.Default())

	actions := []decision.Action{{
		Kind:  decision.ActionKindReply,
		Reply: &decision.ReplyAction{Content: "hello", InReplyTo: "$evt1"},
	}}

	runner.Execute(context.Background(), "!room:test", actions)

	if len(sender.calls) != 1 {
		t.Fatalf("expected 1 call, got %d", len(sender.calls))
	}
	c := sender.calls[0]
	if c.method != "SendReplyMarkdown" {
		t.Errorf("expected SendReplyMarkdown, got %s", c.method)
	}
	if c.inReplyTo != "$evt1" {
		t.Errorf("expected inReplyTo=$evt1, got %s", c.inReplyTo)
	}
}

func TestExecuteReply_WithThread(t *testing.T) {
	sender := &fakeSender{}
	runner := NewRunner(sender, nil, slog.Default())

	actions := []decision.Action{{
		Kind: decision.ActionKindReply,
		Reply: &decision.ReplyAction{
			Content:   "thread reply",
			ThreadID:  "$root",
			InReplyTo: "$evt2",
		},
	}}

	runner.Execute(context.Background(), "!room:test", actions)

	if len(sender.calls) != 1 {
		t.Fatalf("expected 1 call, got %d", len(sender.calls))
	}
	c := sender.calls[0]
	if c.method != "SendThreadMarkdown" {
		t.Errorf("expected SendThreadMarkdown, got %s", c.method)
	}
	if c.threadRootID != "$root" {
		t.Errorf("expected threadRootID=$root, got %s", c.threadRootID)
	}
	if c.inReplyTo != "$evt2" {
		t.Errorf("expected inReplyTo=$evt2, got %s", c.inReplyTo)
	}
	if c.roomID != "!room:test" {
		t.Errorf("expected room !room:test, got %s", c.roomID)
	}
}

func TestExecuteReply_ThreadWithoutInReplyTo(t *testing.T) {
	sender := &fakeSender{}
	runner := NewRunner(sender, nil, slog.Default())

	actions := []decision.Action{{
		Kind: decision.ActionKindReply,
		Reply: &decision.ReplyAction{
			Content:  "thread reply no fallback",
			ThreadID: "$root",
		},
	}}

	runner.Execute(context.Background(), "!room:test", actions)

	if len(sender.calls) != 1 {
		t.Fatalf("expected 1 call, got %d", len(sender.calls))
	}
	c := sender.calls[0]
	if c.method != "SendThreadMarkdown" {
		t.Errorf("expected SendThreadMarkdown, got %s", c.method)
	}
	// inReplyTo should be empty; SendThreadMarkdown will default to threadRootID
	if c.inReplyTo != "" {
		t.Errorf("expected empty inReplyTo, got %s", c.inReplyTo)
	}
}

func TestExecuteReply_NilReply(t *testing.T) {
	sender := &fakeSender{}
	runner := NewRunner(sender, nil, slog.Default())

	actions := []decision.Action{{
		Kind:  decision.ActionKindReply,
		Reply: nil,
	}}

	results := runner.Execute(context.Background(), "!room:test", actions)
	if len(results) != 1 {
		t.Fatalf("expected 1 result, got %d", len(results))
	}
	if results[0].Err == nil {
		t.Error("expected error for nil reply")
	}
}
