package telegramsink

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

// The turn he types in the chat is the turn the web would run, and the reply
// carries the one gesture beside it.
func TestIntakeRunsTheTurnAndOffersTheCorrection(t *testing.T) {
	b := newFakeBot(t)
	rec := &recorder{reply: "поняла", traceID: 91}
	p := newTestPoller(t, b, rec)

	p.handle(context.Background(), msg(ownerChat, "  поужинал  "))

	if got := rec.took(); len(got) != 1 || got[0] != "поужинал" {
		t.Fatalf("turns %q, want the trimmed utterance once", got)
	}
	// The dialogue is keyed per chat, so a clarify asked here is not answered on
	// the web.
	if rec.conversation != "telegram:"+ownerChat {
		t.Errorf("conversation %q does not name the chat", rec.conversation)
	}
	sends := b.called("sendMessage")
	if len(sends) != 1 {
		t.Fatalf("%d sends, want 1", len(sends))
	}
	if sends[0].body["text"] != "поняла" {
		t.Errorf("sent %v, want the reply", sends[0].body["text"])
	}
	if sends[0].body["protect_content"] != true {
		t.Error("his own data went out forwardable")
	}
	kb, _ := json.Marshal(sends[0].body["reply_markup"])
	if !strings.Contains(string(kb), "w:91") {
		t.Errorf("keyboard %s does not point at the turn's trace", kb)
	}
}

// A turn nothing persisted has no row to correct, and a button that would name
// one reports a failure he cannot act on.
func TestIntakeSkipsTheGestureWithNoTrace(t *testing.T) {
	b := newFakeBot(t)
	p := newTestPoller(t, b, &recorder{reply: "поняла", traceID: 0})

	p.handle(context.Background(), msg(ownerChat, "привет"))

	sends := b.called("sendMessage")
	if len(sends) != 1 {
		t.Fatalf("%d sends, want 1", len(sends))
	}
	if _, ok := sends[0].body["reply_markup"]; ok {
		t.Error("offered a correction on a turn with no trace")
	}
}

// One chat, and a stranger is not answered at all: a reply confirms the bot
// exists and whose it is.
func TestIntakeIgnoresAnyOtherChat(t *testing.T) {
	b := newFakeBot(t)
	rec := &recorder{reply: "поняла", traceID: 5}
	p := newTestPoller(t, b, rec)

	p.handle(context.Background(), msg("9999", "включи свет"))
	p.handle(context.Background(), update{UpdateID: 8, CallbackQuery: &callbackQuery{
		ID: "cb", Data: "t:5:note", Message: message{Chat: chat{ID: json.Number("9999")}},
	}})

	if got := rec.took(); len(got) != 0 {
		t.Errorf("ran %q for a chat that is not the owner's", got)
	}
	if len(rec.corrections) != 0 {
		t.Errorf("wrote %v from a chat that is not the owner's", rec.corrections)
	}
	if len(b.calls) != 0 {
		t.Errorf("answered a stranger: %v", b.calls)
	}
}

// Tapping "не то" opens the seven and writes nothing yet. The target is worth
// much more than the negative, so it must not be spent before he can name it.
func TestIntakeFirstTapOnlyOpensTheTargets(t *testing.T) {
	b := newFakeBot(t)
	rec := &recorder{}
	p := newTestPoller(t, b, rec)

	p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
		ID: "cb", Data: "w:77", Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
	}})

	if len(rec.corrections) != 0 {
		t.Fatalf("wrote %v before he named a target", rec.corrections)
	}
	if len(b.called("answerCallbackQuery")) != 1 {
		t.Error("left the clock spinning on the button")
	}
	edits := b.called("editMessageReplyMarkup")
	if len(edits) != 1 {
		t.Fatalf("%d edits, want the target row", len(edits))
	}
	kb, _ := json.Marshal(edits[0].body["reply_markup"])
	for _, want := range CorrectionTargets {
		if !strings.Contains(string(kb), `"`+want+`"`) {
			t.Errorf("target row %s is missing %s", kb, want)
		}
	}
	// And the way out, because he opened the row without knowing he had to name
	// anything.
	if !strings.Contains(string(kb), `"t:77:"`) {
		t.Errorf("target row %s prices out the untargeted negative", kb)
	}
}

func TestIntakeWritesTheCorrection(t *testing.T) {
	for _, tc := range []struct {
		name, data, want string
	}{
		{"with a target", "t:77:note", "note"},
		{"untargeted", "t:77:", ""},
	} {
		t.Run(tc.name, func(t *testing.T) {
			b := newFakeBot(t)
			rec := &recorder{}
			p := newTestPoller(t, b, rec)

			p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
				ID: "cb", Data: tc.data, Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
			}})

			if len(rec.corrections) != 1 || rec.corrections[0] != (correction{77, tc.want}) {
				t.Fatalf("corrections %v, want trace 77 → %q", rec.corrections, tc.want)
			}
			// The buttons come off once the gesture is given.
			edits := b.called("editMessageReplyMarkup")
			if len(edits) != 1 {
				t.Fatalf("%d edits, want the keyboard cleared", len(edits))
			}
			kb, _ := json.Marshal(edits[0].body["reply_markup"])
			if strings.Contains(string(kb), "t:77") {
				t.Errorf("keyboard %s still invites a second correction", kb)
			}
		})
	}
}

// A write that failed says so on the button. Silence would read as recorded.
func TestIntakeSaysWhenTheLabelDidNotLand(t *testing.T) {
	b := newFakeBot(t)
	rec := &recorder{correctErr: errors.New("no such routing trace")}
	p := newTestPoller(t, b, rec)

	p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
		ID: "cb", Data: "t:77:fact", Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
	}})

	answers := b.called("answerCallbackQuery")
	if len(answers) != 1 || answers[0].body["text"] == "" {
		t.Fatalf("answers %v, want a toast saying it did not land", answers)
	}
	if len(b.called("editMessageReplyMarkup")) != 0 {
		t.Error("cleared the buttons after a failed write, so he cannot try again")
	}
}

// A question asked while the daemon was down has been answered by him or has
// stopped mattering, and a reminder set from it would land at the wrong time.
func TestIntakeDiscardsTheBacklog(t *testing.T) {
	b := newFakeBot(t, []update{msg(ownerChat, "напомни в 7 позвонить маме")})
	rec := &recorder{reply: "поняла", traceID: 3}
	p := newTestPoller(t, b, rec)

	p.discardBacklog(context.Background())

	if got := rec.took(); len(got) != 0 {
		t.Errorf("answered %q from the overnight queue", got)
	}
	// And the offset moved past it, so the next poll does not see it again.
	if p.offset != 8 {
		t.Errorf("offset %d, want the skipped update acknowledged", p.offset)
	}
}

// The poller does not start without somewhere to send the turn.
func TestNewPollerNeedsATurn(t *testing.T) {
	sink, err := New(Config{BotToken: "t", ChatID: ownerChat})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := NewPoller(sink, nil, nil); err == nil {
		t.Error("built a poller that reads the chat and answers nothing")
	}
	if _, err := NewPoller(nil, func(context.Context, string, string) (string, int64, error) {
		return "", 0, nil
	}, nil); err == nil {
		t.Error("built a poller with no sink to answer through")
	}
}

// A chat id the intake half cannot match is refused before anything reads the
// chat. Config validation calls the same check, so this is the boot error.
func TestValidateIntakeChatID(t *testing.T) {
	for _, ok := range []string{"123", "-1001234567890", " 42 "} {
		if err := ValidateIntakeChatID(ok); err != nil {
			t.Errorf("ValidateIntakeChatID(%q): %v", ok, err)
		}
	}
	for _, bad := range []string{"", "@maven", "-", "12a", "1 2"} {
		if err := ValidateIntakeChatID(bad); err == nil {
			t.Errorf("ValidateIntakeChatID(%q) accepted; want error", bad)
		}
	}
}
