package botlink

import (
	"context"
	"testing"

	flatbuffers "github.com/google/flatbuffers/go"

	"scrabble/pkg/fbs/scrabblefb"
	botlinkv1 "scrabble/pkg/proto/botlink/v1"
	telegramv1 "scrabble/pkg/proto/telegram/v1"
)

// fakeSender records the delivery calls the executor makes.
type fakeSender struct {
	notify      []notifyCall
	text        []textCall
	gate        []gateCall
	invoice     []invoiceCall
	invoiceLink string // CreateInvoiceLink's returned link
	applied     bool   // ApplyChatGate's reported result
	err         error
}

type notifyCall struct {
	chatID                       int64
	text, buttonText, startParam string
}
type textCall struct {
	chatID int64
	text   string
}
type gateCall struct {
	userID int64
	allow  bool
}
type invoiceCall struct {
	title, description, payload string
	amount                      int64
}

func (f *fakeSender) Notify(_ context.Context, chatID int64, text, buttonText, startParam string) error {
	f.notify = append(f.notify, notifyCall{chatID, text, buttonText, startParam})
	return f.err
}

func (f *fakeSender) SendText(_ context.Context, chatID int64, text string) error {
	f.text = append(f.text, textCall{chatID, text})
	return f.err
}

func (f *fakeSender) ApplyChatGate(_ context.Context, userID int64, allow bool) (bool, error) {
	f.gate = append(f.gate, gateCall{userID, allow})
	return f.applied, f.err
}

func (f *fakeSender) CreateInvoiceLink(_ context.Context, title, description, payload string, amountStars int64) (string, error) {
	f.invoice = append(f.invoice, invoiceCall{title, description, payload, amountStars})
	return f.invoiceLink, f.err
}

func yourTurnPayload(gameID string) []byte {
	b := flatbuffers.NewBuilder(0)
	gid := b.CreateString(gameID)
	scrabblefb.YourTurnEventStart(b)
	scrabblefb.YourTurnEventAddGameId(b, gid)
	b.Finish(scrabblefb.YourTurnEventEnd(b))
	return b.FinishedBytes()
}

func notifyCmd(externalID, kind string, payload []byte, language string) *botlinkv1.Command {
	return &botlinkv1.Command{Payload: &botlinkv1.Command_Notify{Notify: &telegramv1.NotifyRequest{
		ExternalId: externalID, Kind: kind, Payload: payload, Language: language,
	}}}
}

func TestExecutorNotifyDelivers(t *testing.T) {
	const gameID = "7c9e6679-7425-40de-944b-e07fc1f90ae7"
	sender := &fakeSender{}
	exec := NewExecutor(sender, 0, nil)
	delivered, _, err := exec.Handle(context.Background(), notifyCmd("12345", "your_turn", yourTurnPayload(gameID), "en"))
	if err != nil {
		t.Fatalf("handle: %v", err)
	}
	if !delivered {
		t.Fatal("expected delivered=true")
	}
	if len(sender.notify) != 1 {
		t.Fatalf("notify calls = %d, want 1", len(sender.notify))
	}
	if got := sender.notify[0]; got.chatID != 12345 || got.startParam != "g"+gameID {
		t.Errorf("notify call = %+v, want chatID 12345 startParam g%s", got, gameID)
	}
}

func TestExecutorNotifySkipsUnrenderedKind(t *testing.T) {
	sender := &fakeSender{}
	exec := NewExecutor(sender, 0, nil)
	delivered, _, err := exec.Handle(context.Background(), notifyCmd("12345", "opponent_moved", nil, "en"))
	if err != nil {
		t.Fatalf("handle: %v", err)
	}
	if delivered {
		t.Error("expected delivered=false for an unrendered kind")
	}
	if len(sender.notify) != 0 {
		t.Errorf("sender called %d times, want 0", len(sender.notify))
	}
}

func TestExecutorNotifyInvalidExternalID(t *testing.T) {
	exec := NewExecutor(&fakeSender{}, 0, nil)
	if _, _, err := exec.Handle(context.Background(), notifyCmd("not-a-number", "your_turn", yourTurnPayload("g"), "en")); err == nil {
		t.Error("expected an error for a non-numeric external_id")
	}
}

func TestExecutorSendToUser(t *testing.T) {
	sender := &fakeSender{}
	exec := NewExecutor(sender, 0, nil)
	cmd := &botlinkv1.Command{Payload: &botlinkv1.Command_SendToUser{SendToUser: &telegramv1.SendToUserRequest{ExternalId: "999", Text: "hi"}}}
	delivered, _, err := exec.Handle(context.Background(), cmd)
	if err != nil {
		t.Fatalf("handle: %v", err)
	}
	if !delivered || len(sender.text) != 1 || sender.text[0].chatID != 999 || sender.text[0].text != "hi" {
		t.Errorf("send to user = %v / calls %+v", delivered, sender.text)
	}
}

func chatGateCmd(externalID string, allow bool) *botlinkv1.Command {
	return &botlinkv1.Command{Payload: &botlinkv1.Command_ChatGate{ChatGate: &botlinkv1.ChatGateCommand{
		ExternalId: externalID, Allow: allow,
	}}}
}

func TestExecutorChatGateApplied(t *testing.T) {
	sender := &fakeSender{applied: true}
	exec := NewExecutor(sender, 0, nil)
	delivered, _, err := exec.Handle(context.Background(), chatGateCmd("777", true))
	if err != nil {
		t.Fatalf("handle: %v", err)
	}
	if !delivered || len(sender.gate) != 1 || sender.gate[0].userID != 777 || !sender.gate[0].allow {
		t.Errorf("chat gate = %v / calls %+v", delivered, sender.gate)
	}
}

func TestExecutorChatGateNotInChat(t *testing.T) {
	sender := &fakeSender{applied: false} // user not in the chat
	exec := NewExecutor(sender, 0, nil)
	delivered, _, err := exec.Handle(context.Background(), chatGateCmd("888", false))
	if err != nil {
		t.Fatalf("handle: %v", err)
	}
	if delivered {
		t.Error("expected delivered=false when the user is not in the chat")
	}
	if len(sender.gate) != 1 {
		t.Errorf("gate calls = %d, want 1", len(sender.gate))
	}
}

func TestExecutorChatGateInvalidExternalID(t *testing.T) {
	exec := NewExecutor(&fakeSender{}, 0, nil)
	if _, _, err := exec.Handle(context.Background(), chatGateCmd("not-a-number", true)); err == nil {
		t.Error("expected an error for a non-numeric external_id")
	}
}

func TestExecutorCreateInvoice(t *testing.T) {
	sender := &fakeSender{invoiceLink: "https://telegram.me/$abc"}
	exec := NewExecutor(sender, 0, nil)
	cmd := &botlinkv1.Command{Payload: &botlinkv1.Command_CreateInvoice{CreateInvoice: &botlinkv1.CreateInvoiceCommand{
		Title: "50 chips", Description: "50 chips", Payload: "order-1", Amount: 40,
	}}}
	delivered, result, err := exec.Handle(context.Background(), cmd)
	if err != nil {
		t.Fatalf("handle: %v", err)
	}
	if !delivered || result != "https://telegram.me/$abc" {
		t.Errorf("create invoice = %v / %q, want true / the link", delivered, result)
	}
	if len(sender.invoice) != 1 || sender.invoice[0].payload != "order-1" || sender.invoice[0].amount != 40 {
		t.Errorf("invoice calls = %+v", sender.invoice)
	}
}

func TestExecutorCreateInvoiceError(t *testing.T) {
	sender := &fakeSender{err: context.DeadlineExceeded}
	exec := NewExecutor(sender, 0, nil)
	cmd := &botlinkv1.Command{Payload: &botlinkv1.Command_CreateInvoice{CreateInvoice: &botlinkv1.CreateInvoiceCommand{
		Title: "x", Description: "x", Payload: "order-2", Amount: 10,
	}}}
	if _, _, err := exec.Handle(context.Background(), cmd); err == nil {
		t.Error("expected an error when minting the invoice fails")
	}
}

func TestExecutorSendToChannel(t *testing.T) {
	channelCmd := &botlinkv1.Command{Payload: &botlinkv1.Command_SendToChannel{SendToChannel: &telegramv1.SendToGameChannelRequest{Text: "news"}}}

	t.Run("unconfigured", func(t *testing.T) {
		exec := NewExecutor(&fakeSender{}, 0, nil)
		if _, _, err := exec.Handle(context.Background(), channelCmd); err == nil {
			t.Error("expected an error when no channel is configured")
		}
	})
	t.Run("configured", func(t *testing.T) {
		sender := &fakeSender{}
		exec := NewExecutor(sender, 555, nil)
		delivered, _, err := exec.Handle(context.Background(), channelCmd)
		if err != nil {
			t.Fatalf("handle: %v", err)
		}
		if !delivered || len(sender.text) != 1 || sender.text[0].chatID != 555 {
			t.Errorf("send to channel: %+v", sender.text)
		}
	})
}
