package main

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

	jobDomain "apps/backend/internal/module/job/domain"
	jobRepo "apps/backend/internal/module/job/repository"
	jobUC "apps/backend/internal/module/job/usecase"
	scoutDomain "apps/backend/internal/module/scout/domain"
	scoutRepo "apps/backend/internal/module/scout/repository"
	scoutUC "apps/backend/internal/module/scout/usecase"
)

type workerScoutProvider struct{ err error }

func (p workerScoutProvider) SearchThreads(_ context.Context, terms []string, limit int) ([]scoutUC.ThreadSearchResult, error) {
	if p.err != nil {
		return nil, p.err
	}
	term := "市集"
	if len(terms) > 0 && terms[0] != "" {
		term = terms[0]
	}
	if limit < 1 {
		limit = 1
	}
	return []scoutUC.ThreadSearchResult{{
		URL: "https://www.threads.net/@worker/post/ok", Snippet: term + "討論內容",
	}}, nil
}

type failingPublishScoutRepo struct{ scoutDomain.Repository }

func (failingPublishScoutRepo) PublishRunPosts(context.Context, int64, string, []*scoutDomain.Post) error {
	return errors.New("publish storage unavailable")
}

func workerPayload(runID string, brief scoutDomain.RunBrief) string {
	raw, _ := json.Marshal(struct {
		RunID string `json:"run_id"`
		scoutDomain.RunBrief
	}{RunID: runID, RunBrief: brief})
	return string(raw)
}

func workerRunAndJob(t *testing.T, scoutStore scoutDomain.Repository, jobStore jobDomain.Repository, status string) (*scoutUC.Service, *jobUC.Service, *jobDomain.Job, *scoutDomain.Run) {
	t.Helper()
	brief := scoutDomain.RunBrief{Intent: "市集", Mode: scoutDomain.ModeActivity, ScanTerms: []string{"市集"}}
	run := scoutDomain.NewRun("worker-run", "worker-job", 7, brief, 1)
	if err := scoutStore.CreateRun(context.Background(), run); err != nil {
		t.Fatal(err)
	}
	job := &jobDomain.Job{ID: run.JobID, OwnerUID: 7, TemplateType: jobDomain.TemplateScoutScan,
		Status: jobDomain.StatusRunning, RefID: run.ID, Payload: workerPayload(run.ID, brief), CreatedAt: 1, UpdatedAt: 1}
	if err := jobStore.Insert(context.Background(), job); err != nil {
		t.Fatal(err)
	}
	if status != scoutDomain.RunQueued {
		if status == scoutDomain.RunCancelled {
			run.Status = scoutDomain.RunCancelled
		} else {
			run.Status = status
		}
		if err := scoutStore.ReplaceRunGuarded(context.Background(), 7, run.ID, []string{scoutDomain.RunQueued}, run); err != nil {
			t.Fatal(err)
		}
	}
	scout := scoutUC.New(scoutStore)
	scout.Provider = workerScoutProvider{}
	return scout, jobUC.New(jobStore), job, run
}

func TestWorkerScoutScanSuccessPublishesExactlyOnce(t *testing.T) {
	ctx := context.Background()
	scoutStore := scoutRepo.NewMemory()
	jobStore := jobRepo.NewMemory()
	scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
	if err := runScoutScan(ctx, jobs, scout, job); err != nil {
		t.Fatal(err)
	}
	gotRun, err := scoutStore.GetRun(ctx, 7, run.ID)
	if err != nil || gotRun.Status != scoutDomain.RunSucceeded || gotRun.EligibleCount != 1 {
		t.Fatalf("run=%+v err=%v", gotRun, err)
	}
	posts, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
	if err != nil || len(posts.Items) != 1 {
		t.Fatalf("published posts=%+v err=%v", posts, err)
	}
	jobAfter, err := jobStore.FindByID(ctx, job.ID)
	if err != nil || jobAfter.Status != jobDomain.StatusSucceeded {
		t.Fatalf("job=%+v err=%v", jobAfter, err)
	}
	if err := runScoutScan(ctx, jobs, scout, job); err == nil {
		t.Fatal("terminal run retry should not search/publish again")
	}
	postsAgain, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
	if err != nil || len(postsAgain.Items) != 1 {
		t.Fatalf("retry changed result membership: %+v %v", postsAgain, err)
	}
}

func TestWorkerScoutScanMismatchFailsBeforeSearch(t *testing.T) {
	ctx := context.Background()
	scoutStore := scoutRepo.NewMemory()
	jobStore := jobRepo.NewMemory()
	scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
	job.RefID = "wrong-run"
	if err := runScoutScan(ctx, jobs, scout, job); err == nil {
		t.Fatal("reference mismatch should fail")
	}
	got, err := scoutStore.GetRun(ctx, 7, run.ID)
	if err != nil || got.Status != scoutDomain.RunQueued {
		t.Fatalf("mismatch changed run=%+v err=%v", got, err)
	}
}

func TestWorkerScoutScanProviderFailureFailsRunAndHidesResults(t *testing.T) {
	ctx := context.Background()
	scoutStore := scoutRepo.NewMemory()
	jobStore := jobRepo.NewMemory()
	scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
	scout.Provider = workerScoutProvider{err: errors.New("provider timeout: token redacted")}
	if err := runScoutScan(ctx, jobs, scout, job); err == nil {
		t.Fatal("provider failure should fail")
	}
	got, err := scoutStore.GetRun(ctx, 7, run.ID)
	if err != nil || got.Status != scoutDomain.RunFailed || got.Error == "" {
		t.Fatalf("failed run=%+v err=%v", got, err)
	}
	posts, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
	if err != nil || len(posts.Items) != 0 {
		t.Fatalf("failed run exposed posts=%+v err=%v", posts, err)
	}
}

func TestWorkerScoutScanPublishFailureFailsRun(t *testing.T) {
	ctx := context.Background()
	base := scoutRepo.NewMemory()
	scoutStore := failingPublishScoutRepo{Repository: base}
	jobStore := jobRepo.NewMemory()
	scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
	if err := runScoutScan(ctx, jobs, scout, job); err == nil {
		t.Fatal("publish failure should fail")
	}
	got, err := base.GetRun(ctx, 7, run.ID)
	if err != nil || got.Status != scoutDomain.RunFailed {
		t.Fatalf("publish failed run=%+v err=%v", got, err)
	}
}

func TestWorkerScoutScanCancelledRunDoesNotRestart(t *testing.T) {
	ctx := context.Background()
	scoutStore := scoutRepo.NewMemory()
	jobStore := jobRepo.NewMemory()
	scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunCancelled)
	if err := runScoutScan(ctx, jobs, scout, job); err == nil {
		t.Fatal("cancelled run should not restart")
	}
	got, err := scoutStore.GetRun(ctx, 7, run.ID)
	if err != nil || got.Status != scoutDomain.RunCancelled {
		t.Fatalf("cancelled run changed=%+v err=%v", got, err)
	}
}
