package usecase_test

import (
	"context"
	"testing"

	"apps/backend/internal/module/threads/domain"
	"apps/backend/internal/module/threads/provider"
	"apps/backend/internal/module/threads/repository"
	"apps/backend/internal/module/threads/usecase"

	"github.com/stretchr/testify/require"
)

func newThreadsSvc(failEx, failRef bool) (*usecase.Service, *repository.MemoryStore, *provider.FakeProvider) {
	store := repository.NewMemory()
	fake := &provider.FakeProvider{
		CallbackBase: "http://127.0.0.1:8888/api/v1/threads-accounts/oauth/callback",
		FailExchange: failEx,
		FailRefresh:  failRef,
	}
	svc := usecase.New(store, fake, "test-secret-key")
	svc.APIPublicBase = "http://127.0.0.1:8888"
	svc.WebBase = "http://127.0.0.1:5173"
	return svc, store, fake
}

func TestTH_01_OAuthStart(t *testing.T) {
	svc, _, _ := newThreadsSvc(false, false)
	url, state, err := svc.OAuthStart(context.Background(), 1_000_001)
	require.NoError(t, err)
	require.NotEmpty(t, state)
	require.Contains(t, url, "state=")
	require.Contains(t, url, "code=fake")
}

func TestTH_02_CallbackSuccess(t *testing.T) {
	svc, store, _ := newThreadsSvc(false, false)
	uid := int64(1_000_002)
	_, state, err := svc.OAuthStart(context.Background(), uid)
	require.NoError(t, err)
	acc, err := svc.OAuthCallback(context.Background(), "fake", state, "")
	require.NoError(t, err)
	require.Equal(t, domain.ConnectionConnected, acc.Connection)
	require.True(t, acc.IsUsable)
	require.Equal(t, uid, acc.OwnerUID)
	require.NotEmpty(t, acc.AccessTokenEnc)
	// token not equal plaintext in public sense
	require.NotContains(t, acc.AccessTokenEnc, "fake-access")
	list, err := store.ListByOwner(context.Background(), uid)
	require.NoError(t, err)
	require.Len(t, list, 1)
}

func TestTH_03_UserDenied(t *testing.T) {
	svc, _, _ := newThreadsSvc(false, false)
	_, state, err := svc.OAuthStart(context.Background(), 1_000_003)
	require.NoError(t, err)
	_, err = svc.OAuthCallback(context.Background(), "", state, "access_denied")
	require.ErrorIs(t, err, domain.ErrOAuthDenied)
}

func TestTH_04_TokenExchangeFail(t *testing.T) {
	svc, _, _ := newThreadsSvc(true, false)
	_, state, err := svc.OAuthStart(context.Background(), 1_000_004)
	require.NoError(t, err)
	_, err = svc.OAuthCallback(context.Background(), "fake", state, "")
	require.ErrorIs(t, err, domain.ErrOAuthExchange)
}

func TestTH_05_List(t *testing.T) {
	svc, _, _ := newThreadsSvc(false, false)
	uid := int64(1_000_005)
	_, st, _ := svc.OAuthStart(context.Background(), uid)
	_, err := svc.OAuthCallback(context.Background(), "fake", st, "")
	require.NoError(t, err)
	list, err := svc.List(context.Background(), uid)
	require.NoError(t, err)
	require.Len(t, list, 1)
	require.True(t, list[0].SessionExpiresAt > 0)
}

func TestTH_06_RefreshOk(t *testing.T) {
	svc, _, _ := newThreadsSvc(false, false)
	uid := int64(1_000_006)
	_, st, _ := svc.OAuthStart(context.Background(), uid)
	acc, err := svc.OAuthCallback(context.Background(), "fake", st, "")
	require.NoError(t, err)
	out, err := svc.Refresh(context.Background(), uid, acc.ID)
	require.NoError(t, err)
	require.True(t, out.IsUsable)
	require.Equal(t, domain.ConnectionConnected, out.Connection)
	require.True(t, out.SessionRefreshedAt >= acc.SessionRefreshedAt)
}

func TestTH_07_RefreshFail(t *testing.T) {
	svc, store, fake := newThreadsSvc(false, false)
	uid := int64(1_000_007)
	_, st, _ := svc.OAuthStart(context.Background(), uid)
	acc, err := svc.OAuthCallback(context.Background(), "fake", st, "")
	require.NoError(t, err)
	fake.FailRefresh = true
	// poison refresh token
	acc.RefreshTokenEnc, _ = domain.Seal("test-secret-key", "bad-token")
	require.NoError(t, store.Update(context.Background(), acc))
	out, err := svc.Refresh(context.Background(), uid, acc.ID)
	require.ErrorIs(t, err, domain.ErrRefreshFailed)
	require.False(t, out.IsUsable)
	require.Equal(t, domain.ConnectionError, out.Connection)
}

func TestTH_08_Disconnect(t *testing.T) {
	svc, _, _ := newThreadsSvc(false, false)
	uid := int64(1_000_008)
	_, st, _ := svc.OAuthStart(context.Background(), uid)
	acc, err := svc.OAuthCallback(context.Background(), "fake", st, "")
	require.NoError(t, err)
	require.NoError(t, svc.Disconnect(context.Background(), uid, acc.ID))
	list, err := svc.List(context.Background(), uid)
	require.NoError(t, err)
	require.Empty(t, list)
}

func TestTH_11_ReconnectUpsertsSession(t *testing.T) {
	svc, _, fake := newThreadsSvc(false, false)
	fake.FixedUserID = "threads_uid_fixed"
	fake.FixedUsername = "harbor_main"
	uid := int64(1_000_012)

	_, st1, _ := svc.OAuthStart(context.Background(), uid)
	acc1, err := svc.OAuthCallback(context.Background(), "fake", st1, "")
	require.NoError(t, err)
	oldTok := acc1.AccessTokenEnc

	_, st2, _ := svc.OAuthStart(context.Background(), uid)
	acc2, err := svc.OAuthCallback(context.Background(), "fake", st2, "")
	require.NoError(t, err)
	require.Equal(t, acc1.ID, acc2.ID, "same Threads user must keep same account id")
	require.NotEqual(t, oldTok, acc2.AccessTokenEnc, "session tokens must be refreshed")
	require.True(t, acc2.IsUsable)
	require.Equal(t, domain.ConnectionConnected, acc2.Connection)

	list, err := svc.List(context.Background(), uid)
	require.NoError(t, err)
	require.Len(t, list, 1)
}

func TestTH_09_RequiresCallbackNotBypass(t *testing.T) {
	// Live completion requires OAuthCallback path (has encrypted token).
	svc, _, _ := newThreadsSvc(false, false)
	uid := int64(1_000_009)
	_, st, _ := svc.OAuthStart(context.Background(), uid)
	acc, err := svc.OAuthCallback(context.Background(), "fake", st, "")
	require.NoError(t, err)
	require.NotEmpty(t, acc.AccessTokenEnc)
	require.True(t, acc.IsUsable)
}

func TestTH_10_CrossUidDenied(t *testing.T) {
	svc, _, _ := newThreadsSvc(false, false)
	uidA := int64(1_000_010)
	uidB := int64(1_000_011)
	_, st, _ := svc.OAuthStart(context.Background(), uidA)
	acc, err := svc.OAuthCallback(context.Background(), "fake", st, "")
	require.NoError(t, err)
	_, err = svc.Refresh(context.Background(), uidB, acc.ID)
	require.ErrorIs(t, err, domain.ErrForbidden)
	err = svc.Disconnect(context.Background(), uidB, acc.ID)
	require.ErrorIs(t, err, domain.ErrForbidden)
}
