package usecase_test

import (
	"context"
	"fmt"
	"strings"
	"testing"

	"apps/backend/internal/module/scout/domain"
	"apps/backend/internal/module/scout/repository"
	"apps/backend/internal/module/scout/usecase"
	studioPublish "apps/backend/internal/module/studio/publish"

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

type staticDevMode struct {
	on bool
}

type fakeThreadSearchProvider struct{}

type fakeChromeCrawler struct{ fakeThreadSearchProvider }

type failingChromeCrawler struct{ fakeChromeCrawler }

type fakeReplyQueue struct{}

func (fakeThreadSearchProvider) SearchThreads(_ context.Context, terms []string, limit int) ([]usecase.ThreadSearchResult, error) {
	results := make([]usecase.ThreadSearchResult, 0, limit)
	for i, term := range terms {
		if i == limit {
			break
		}
		results = append(results, usecase.ThreadSearchResult{
			URL:     "https://www.threads.net/@test_user/post/" + term,
			Title:   term,
			Snippet: "realistic result for " + term,
		})
	}
	return results, nil
}

func (f fakeChromeCrawler) SearchChrome(ctx context.Context, storageState string, terms []string, limit int) ([]usecase.ThreadSearchResult, error) {
	if strings.TrimSpace(storageState) == "" {
		return nil, domain.ErrNoCrawlerSession
	}
	return f.SearchThreads(ctx, terms, limit)
}

func (fakeChromeCrawler) ResolveMediaID(_ context.Context, _ string, _ string) (string, error) {
	return "123456789012345", nil
}

func (failingChromeCrawler) ResolveMediaID(_ context.Context, _ string, _ string) (string, error) {
	return "", fmt.Errorf("crawler session expired")
}

func (fakeReplyQueue) QueueExternalReply(_ context.Context, _ int64, _, _, _, _ string) (string, error) {
	return "outbox_test", nil
}

func (s *staticDevMode) DevModeEnabled(_ context.Context, _ int64) (bool, error) {
	return s.on, nil
}

func newScout() (*usecase.Service, *studioPublish.FakeTransport, *staticDevMode) {
	tp := studioPublish.NewFake()
	dev := &staticDevMode{}
	svc := usecase.New(repository.NewMemory())
	svc.SessionSecret = "test-crawler-session-secret"
	svc.Transport = tp
	svc.Settings = dev
	svc.Provider = fakeThreadSearchProvider{}
	svc.Crawler = fakeChromeCrawler{}
	return svc, tp, dev
}

func TestSC_01_CreateBrandProduct(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_001)
	b, err := svc.CreateBrand(context.Background(), uid, "海港", "敏感肌")
	require.NoError(t, err)
	p, err := svc.SaveProduct(context.Background(), uid, &domain.Product{
		BrandID: b.ID, Label: "無香洗髮", PainPoints: []string{"刺鼻"}, MatchTags: []string{"洗髮"},
	})
	require.NoError(t, err)
	list, err := svc.ListProducts(context.Background(), uid, b.ID)
	require.NoError(t, err)
	require.Len(t, list, 1)
	require.Equal(t, p.ID, list[0].ID)
}

func TestSC_02_DeleteBrandWithProductsRefused(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_002)
	b, _ := svc.CreateBrand(context.Background(), uid, "B", "")
	_, _ = svc.SaveProduct(context.Background(), uid, &domain.Product{BrandID: b.ID, Label: "P"})
	err := svc.RemoveBrand(context.Background(), uid, b.ID)
	require.ErrorIs(t, err, domain.ErrHasProducts)
}

func TestSC_03_PrepareBrief(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_003)
	brief, err := svc.PrepareBrief(context.Background(), uid, "敏感肌保養", "", "", "value", false)
	require.NoError(t, err)
	require.NotEmpty(t, brief.ScanTerms)
	require.Equal(t, "敏感肌保養", brief.Intent)
}

func TestSC_04_PrepareBriefEmptyIntent(t *testing.T) {
	svc, _, _ := newScout()
	_, err := svc.PrepareBrief(context.Background(), 1, "  ", "", "", "", false)
	require.Error(t, err)
}

func TestSC_05_ApiPath(t *testing.T) {
	svc, _, dev := newScout()
	uid := int64(5_002_005)
	dev.on = false
	brief, _ := svc.PrepareBrief(context.Background(), uid, "遠端辦公", "", "", "value", false)
	posts, err := svc.RunScanFromBrief(context.Background(), uid, brief)
	require.NoError(t, err)
	require.NotEmpty(t, posts)
	require.Equal(t, domain.PathAPI, posts[0].ScanPath)
}

func TestSC_06_CrawlerPath(t *testing.T) {
	svc, _, dev := newScout()
	uid := int64(5_002_006)
	dev.on = true
	require.NoError(t, svc.SetCrawlerSession(context.Background(), uid, validStorageState()))
	brief, _ := svc.PrepareBrief(context.Background(), uid, "成分黨", "", "", "value", false)
	posts, err := svc.RunScanFromBrief(context.Background(), uid, brief)
	require.NoError(t, err)
	require.Equal(t, domain.PathCrawler, posts[0].ScanPath)
}

func TestSC_07_NoSession(t *testing.T) {
	svc, _, dev := newScout()
	uid := int64(5_002_007)
	dev.on = true
	brief, _ := svc.PrepareBrief(context.Background(), uid, "無 session", "", "", "value", false)
	_, err := svc.RunScanFromBrief(context.Background(), uid, brief)
	require.ErrorIs(t, err, domain.ErrNoCrawlerSession)
}

func TestCrawlerSessionEncryptedAndValidated(t *testing.T) {
	ctx := context.Background()
	store := repository.NewMemory()
	svc := usecase.New(store)
	svc.SessionSecret = "test-crawler-session-secret"
	state := validStorageState()

	require.NoError(t, svc.SetCrawlerSession(ctx, 1, state))
	persisted, err := store.GetCrawlerSession(ctx, 1)
	require.NoError(t, err)
	require.NotEqual(t, state, persisted.StorageStateEnc)
	require.Positive(t, persisted.UpdatedAt)
	require.Positive(t, persisted.ExpiresAt)
	got, err := svc.GetCrawlerSessionToken(ctx, 1)
	require.NoError(t, err)
	require.Equal(t, state, got)

	for _, invalid := range []string{
		"not json",
		"[]",
		`{"cookies":[]}`,
		`{"cookies":[{"domain":"evil.example","expires":4102444800}]}`,
		`{"cookies":null}`,
		strings.Repeat("x", 256*1024+1),
	} {
		require.ErrorIs(t, svc.SetCrawlerSession(ctx, 1, invalid), domain.ErrValidation)
	}
}

func TestCrawlerSessionRequiresSecret(t *testing.T) {
	svc := usecase.New(repository.NewMemory())
	require.ErrorIs(t, svc.SetCrawlerSession(context.Background(), 1, validStorageState()), domain.ErrValidation)
}

func validStorageState() string {
	return `{"cookies":[{"domain":".threads.net","expires":4102444800}]}`
}

func TestSC_08_ListPostsAfterScan(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_008)
	brief, _ := svc.PrepareBrief(context.Background(), uid, "list test", "", "", "value", false)
	_, _ = svc.RunScanFromBrief(context.Background(), uid, brief)
	list, err := svc.ListPosts(context.Background(), uid, "")
	require.NoError(t, err)
	require.NotEmpty(t, list)
	require.True(t, list[0].CreatedAt > 0)
}

func TestSC_09_DraftOutreach(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_009)
	brief, _ := svc.PrepareBrief(context.Background(), uid, "draft", "", "", "value", false)
	posts, _ := svc.RunScanFromBrief(context.Background(), uid, brief)
	p, err := svc.DraftOutreach(context.Background(), uid, posts[0].ID, "")
	require.NoError(t, err)
	require.Equal(t, domain.OutreachDrafted, p.OutreachStatus)
	require.NotEmpty(t, p.DraftText)
}

func TestSC_10_SkipAndMark(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_010)
	brief, _ := svc.PrepareBrief(context.Background(), uid, "skip", "", "", "value", false)
	posts, _ := svc.RunScanFromBrief(context.Background(), uid, brief)
	p, err := svc.SkipOutreach(context.Background(), uid, posts[0].ID)
	require.NoError(t, err)
	require.Equal(t, domain.OutreachSkipped, p.OutreachStatus)
	p, err = svc.MarkPublished(context.Background(), uid, posts[0].ID)
	require.NoError(t, err)
	require.Equal(t, domain.OutreachPublished, p.OutreachStatus)
}

func TestSC_11_SendOutreach(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_011)
	brief, _ := svc.PrepareBrief(context.Background(), uid, "send", "", "", "value", false)
	posts, _ := svc.RunScanFromBrief(context.Background(), uid, brief)
	_, err := svc.SendOutreach(context.Background(), uid, posts[0].ID, "你好呀", "acc1")
	require.Error(t, err)
}

func TestSC_11_SendOutreachResolverFailureIsValidation(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_012)
	svc.Crawler = failingChromeCrawler{}
	svc.ReplyQueue = fakeReplyQueue{}
	require.NoError(t, svc.SetCrawlerSession(context.Background(), uid, validStorageState()))
	brief, _ := svc.PrepareBrief(context.Background(), uid, "send", "", "", "value", false)
	posts, _ := svc.RunScanFromBrief(context.Background(), uid, brief)

	_, err := svc.SendOutreach(context.Background(), uid, posts[0].ID, "你好呀", "acc1")
	require.ErrorIs(t, err, domain.ErrValidation)
	require.ErrorContains(t, err, "unable to resolve the target Threads post")
}

func TestSC_12_RemovePostTheme(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_012)
	brief, _ := svc.PrepareBrief(context.Background(), uid, "rm", "", "", "value", false)
	posts, _ := svc.RunScanFromBrief(context.Background(), uid, brief)
	require.NoError(t, svc.RemovePost(context.Background(), uid, posts[0].ID))
	require.NoError(t, svc.RemoveTheme(context.Background(), uid, brief.ThemeKey))
	list, _ := svc.ListPosts(context.Background(), uid, "")
	require.Empty(t, list)
}

func TestSC_13_Homework(t *testing.T) {
	svc, _, _ := newScout()
	uid := int64(5_002_013)
	brief, _ := svc.PrepareBrief(context.Background(), uid, "hw", "", "", "value", false)
	h, err := svc.SaveHomework(context.Background(), uid, &domain.Homework{
		ThemeKey: brief.ThemeKey, ThemeLabel: brief.ThemeLabel, Purpose: "value", Brief: *brief,
	})
	require.NoError(t, err)
	list, err := svc.ListHomework(context.Background(), uid)
	require.NoError(t, err)
	require.Len(t, list, 1)
	got, err := svc.GetHomework(context.Background(), uid, h.ThemeKey)
	require.NoError(t, err)
	require.Equal(t, brief.Intent, got.Brief.Intent)
	require.NoError(t, svc.RemoveHomework(context.Background(), uid, h.ThemeKey))
}

func TestSC_14_ImportProductFromUrl(t *testing.T) {
	svc, _, _ := newScout()
	d, err := svc.ImportProductFromURL(context.Background(), "https://shop.example.com/item/1")
	require.NoError(t, err)
	require.NotEmpty(t, d.Label)
	require.Equal(t, "https://shop.example.com/item/1", d.PlacementURL)
}

func TestSC_TopicRemoved(t *testing.T) {
	svc, _, _ := newScout()
	require.ErrorIs(t, svc.TopicRemoved(), domain.ErrTopicRemoved)
}
