package waitlist_test

import (
	"context"
	"os"
	"testing"

	pbaero "github.com/flowy-live/llink/genproto/aero"
	"github.com/flowy-live/llink/internal/testhelper"
	"github.com/flowy-live/llink/internal/testhelper/mocks/aero"
	"github.com/flowy-live/llink/internal/waitlist"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/stretchr/testify/assert"
	"go.uber.org/mock/gomock"
)

var dbPool *pgxpool.Pool

func TestMain(m *testing.M) {
	dbPool = testhelper.SetupTestDB()
	defer testhelper.TeardownTestDB()

	ret := m.Run()
	os.Exit(ret)
}

func newTestService(t *testing.T) waitlist.Service {
	ctrl := gomock.NewController(t)
	mockAero := aero.NewMockPrimaryClient(ctrl)
	mockAero.EXPECT().
		ShootEmail(gomock.Any(), gomock.Any()).
		Return(&pbaero.ShootEmailResponse{}, nil).
		AnyTimes()
	return waitlist.NewService(dbPool, mockAero)
}

func TestWaitlistService_AddAndGet(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	email := "newuser@example.com"
	metadata := map[string]string{"source": "landing-page", "plan": "pro"}

	// Add to waitlist
	err := svc.AddToWaitlist(ctx, email, metadata)
	assert.NoError(t, err)

	// Get entry by email
	entry, err := svc.GetWaitlistEntryByEmail(ctx, email)
	assert.NoError(t, err)
	assert.Equal(t, email, entry.Email)
	assert.Equal(t, "landing-page", entry.Metadata["source"])
	assert.Equal(t, "pro", entry.Metadata["plan"])
	assert.NotZero(t, entry.CreatedAt)
	assert.Nil(t, entry.InvitedAt)

	// Clean up
	_, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, email)
	assert.NoError(t, err)
}

func TestWaitlistService_AddDuplicate(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	email := "duplicate@example.com"

	err := svc.AddToWaitlist(ctx, email, nil)
	assert.NoError(t, err)

	// Adding the same email again should return AlreadyInWaitlistError
	err = svc.AddToWaitlist(ctx, email, nil)
	assert.ErrorIs(t, err, waitlist.AlreadyInWaitlistError)

	// Clean up
	_, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, email)
	assert.NoError(t, err)
}

func TestWaitlistService_AddNilMetadata(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	email := "nilmeta@example.com"

	err := svc.AddToWaitlist(ctx, email, nil)
	assert.NoError(t, err)

	entry, err := svc.GetWaitlistEntryByEmail(ctx, email)
	assert.NoError(t, err)
	assert.NotNil(t, entry.Metadata)

	// Clean up
	_, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, email)
	assert.NoError(t, err)
}

func TestWaitlistService_GetEntryNotFound(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	_, err := svc.GetWaitlistEntryByEmail(ctx, "nonexistent@example.com")
	assert.ErrorIs(t, err, waitlist.EntryNotFoundError)
}

func TestWaitlistService_MarkInvited(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	email := "invite-me@example.com"

	err := svc.AddToWaitlist(ctx, email, nil)
	assert.NoError(t, err)

	// Mark as invited
	err = svc.MarkWaitlistEntryInvited(ctx, email)
	assert.NoError(t, err)

	// Verify invited_at is set
	entry, err := svc.GetWaitlistEntryByEmail(ctx, email)
	assert.NoError(t, err)
	assert.NotNil(t, entry.InvitedAt)

	// Clean up
	_, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, email)
	assert.NoError(t, err)
}

func TestWaitlistService_MarkInvitedNotFound(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	err := svc.MarkWaitlistEntryInvited(ctx, "ghost@example.com")
	assert.ErrorIs(t, err, waitlist.EntryNotFoundError)
}

func TestWaitlistService_GetWaitlistFilters(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	uninvitedEmail := "uninvited-filter@example.com"
	invitedEmail := "invited-filter@example.com"

	// Add two entries
	err := svc.AddToWaitlist(ctx, uninvitedEmail, nil)
	assert.NoError(t, err)
	err = svc.AddToWaitlist(ctx, invitedEmail, nil)
	assert.NoError(t, err)

	// Mark one as invited
	err = svc.MarkWaitlistEntryInvited(ctx, invitedEmail)
	assert.NoError(t, err)

	// Filter: all
	entries, err := svc.GetWaitlist(ctx, waitlist.GetWaitlistFilterAll)
	assert.NoError(t, err)
	emails := extractEmails(entries)
	assert.Contains(t, emails, uninvitedEmail)
	assert.Contains(t, emails, invitedEmail)

	// Filter: invited only
	entries, err = svc.GetWaitlist(ctx, waitlist.GetWaitlistFilterInvitedOnly)
	assert.NoError(t, err)
	emails = extractEmails(entries)
	assert.Contains(t, emails, invitedEmail)
	assert.NotContains(t, emails, uninvitedEmail)

	// Filter: uninvited only
	entries, err = svc.GetWaitlist(ctx, waitlist.GetWaitlistFilterUninvitedOnly)
	assert.NoError(t, err)
	emails = extractEmails(entries)
	assert.Contains(t, emails, uninvitedEmail)
	assert.NotContains(t, emails, invitedEmail)

	// Clean up
	_, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email IN ($1, $2)`, uninvitedEmail, invitedEmail)
	assert.NoError(t, err)
}

func TestWaitlistService_EmailNormalization(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	// Add with uppercase email
	err := svc.AddToWaitlist(ctx, "UPPER@EXAMPLE.COM", nil)
	assert.NoError(t, err)

	// Should find with lowercase
	entry, err := svc.GetWaitlistEntryByEmail(ctx, "upper@example.com")
	assert.NoError(t, err)
	assert.Equal(t, "upper@example.com", entry.Email)

	// Adding with different case should be a duplicate
	err = svc.AddToWaitlist(ctx, "Upper@Example.Com", nil)
	assert.ErrorIs(t, err, waitlist.AlreadyInWaitlistError)

	// Clean up
	_, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, "upper@example.com")
	assert.NoError(t, err)
}

func TestWaitlistService_InvalidEmail(t *testing.T) {
	ctx := context.Background()
	svc := newTestService(t)

	err := svc.AddToWaitlist(ctx, "not-an-email", nil)
	assert.Error(t, err)

	_, err = svc.GetWaitlistEntryByEmail(ctx, "not-an-email")
	assert.Error(t, err)

	err = svc.MarkWaitlistEntryInvited(ctx, "not-an-email")
	assert.Error(t, err)
}

func extractEmails(entries []*waitlist.WaitlistEntry) []string {
	emails := make([]string, len(entries))
	for i, e := range entries {
		emails[i] = e.Email
	}
	return emails
}
