//go:build integration

package inttest

import (
	"context"
	"errors"
	"testing"

	"github.com/google/uuid"

	"scrabble/backend/internal/account"
)

// emailOf returns the external id of the account's email identity, or "" when it has none.
func emailOf(t *testing.T, store *account.Store, id uuid.UUID) string {
	t.Helper()
	ids, err := store.Identities(context.Background(), id)
	if err != nil {
		t.Fatalf("identities %s: %v", id, err)
	}
	for _, i := range ids {
		if i.Kind == account.KindEmail {
			return i.ExternalID
		}
	}
	return ""
}

// TestUnlinkProviderKeepsOthers detaches one provider from a multi-identity account and
// refuses to remove the last remaining identity.
func TestUnlinkProviderKeepsOthers(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)

	acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
	if err != nil {
		t.Fatalf("provision telegram: %v", err)
	}
	email := "unlink-" + uuid.NewString() + "@example.com"
	if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, email, true); err != nil {
		t.Fatalf("attach email: %v", err)
	}

	// Removing a kind the account does not hold reports not-found.
	if err := store.RemoveIdentity(ctx, acc.ID, account.KindVK); !errors.Is(err, account.ErrNotFound) {
		t.Fatalf("remove absent vk = %v, want ErrNotFound", err)
	}

	// Detach Telegram: the email identity remains.
	if err := store.RemoveIdentity(ctx, acc.ID, account.KindTelegram); err != nil {
		t.Fatalf("remove telegram: %v", err)
	}
	ids, err := store.Identities(ctx, acc.ID)
	if err != nil {
		t.Fatalf("identities: %v", err)
	}
	if len(ids) != 1 || ids[0].Kind != account.KindEmail {
		t.Fatalf("identities after unlink = %+v, want only email", ids)
	}

	// The email is now the last identity, so unlinking it is refused.
	if err := store.RemoveIdentity(ctx, acc.ID, account.KindEmail); !errors.Is(err, account.ErrLastIdentity) {
		t.Fatalf("remove last email = %v, want ErrLastIdentity", err)
	}
}

// TestChangeEmailReplaces switches an account's confirmed email to a free address,
// freeing the old one.
func TestChangeEmailReplaces(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)
	mailer := &capturingMailer{}
	svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")

	acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
	if err != nil {
		t.Fatalf("provision: %v", err)
	}
	oldAddr := "old-" + uuid.NewString() + "@example.com"
	if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, oldAddr, true); err != nil {
		t.Fatalf("attach old email: %v", err)
	}
	newAddr := "new-" + uuid.NewString() + "@example.com"

	if err := svc.RequestChangeCode(ctx, acc.ID, newAddr); err != nil {
		t.Fatalf("request change: %v", err)
	}
	code := sixDigit.FindString(mailer.lastBody)
	if _, err := svc.ConfirmChange(ctx, acc.ID, newAddr, code); err != nil {
		t.Fatalf("confirm change: %v", err)
	}
	if got := emailOf(t, store, acc.ID); got != newAddr {
		t.Fatalf("email after change = %q, want %q", got, newAddr)
	}
	if !identityConfirmed(t, account.KindEmail, newAddr) {
		t.Error("the new email identity must be confirmed")
	}
	// The old address is freed: another account can now claim it.
	other, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
	if err != nil {
		t.Fatalf("provision other: %v", err)
	}
	if err := store.AttachIdentity(ctx, other.ID, account.KindEmail, oldAddr, true); err != nil {
		t.Fatalf("old address should be free after change, got: %v", err)
	}
}

// TestChangeEmailRefusesTaken refuses (without merging) a new address already confirmed by
// another account, leaving the caller's email untouched.
func TestChangeEmailRefusesTaken(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)
	mailer := &capturingMailer{}
	svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")

	acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
	if err != nil {
		t.Fatalf("provision caller: %v", err)
	}
	mine := "mine-" + uuid.NewString() + "@example.com"
	if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, mine, true); err != nil {
		t.Fatalf("attach caller email: %v", err)
	}
	other, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
	if err != nil {
		t.Fatalf("provision other: %v", err)
	}
	taken := "taken-" + uuid.NewString() + "@example.com"
	if err := store.AttachIdentity(ctx, other.ID, account.KindEmail, taken, true); err != nil {
		t.Fatalf("attach other email: %v", err)
	}

	if err := svc.RequestChangeCode(ctx, acc.ID, taken); err != nil {
		t.Fatalf("request change: %v", err)
	}
	code := sixDigit.FindString(mailer.lastBody)
	if _, err := svc.ConfirmChange(ctx, acc.ID, taken, code); !errors.Is(err, account.ErrEmailTaken) {
		t.Fatalf("confirm change to taken = %v, want ErrEmailTaken", err)
	}
	if got := emailOf(t, store, acc.ID); got != mine {
		t.Errorf("caller email after refused change = %q, want unchanged %q", got, mine)
	}
}

// TestChangeEmailViaDeeplink switches the email through the one-tap confirm token.
func TestChangeEmailViaDeeplink(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)
	mailer := &capturingMailer{}
	svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")

	acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
	if err != nil {
		t.Fatalf("provision: %v", err)
	}
	if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, "old-"+uuid.NewString()+"@example.com", true); err != nil {
		t.Fatalf("attach old email: %v", err)
	}
	newAddr := "dl-" + uuid.NewString() + "@example.com"
	if err := svc.RequestChangeCode(ctx, acc.ID, newAddr); err != nil {
		t.Fatalf("request change: %v", err)
	}
	res, err := svc.ConfirmByToken(ctx, tokenFromMail(t, mailer.lastBody))
	if err != nil {
		t.Fatalf("confirm by token: %v", err)
	}
	if res.IsLogin() || res.NeedsMerge || res.Account != acc.ID {
		t.Fatalf("deeplink change result = %+v, want a plain change for %s", res, acc.ID)
	}
	if got := emailOf(t, store, acc.ID); got != newAddr {
		t.Fatalf("email after deeplink change = %q, want %q", got, newAddr)
	}
}
