package scout

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

	"apps/backend/internal/middleware"
	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"
	"apps/backend/internal/svc"
	"apps/backend/internal/types"
)

func TestRunScanCreatesIndependentRunAndJobLink(t *testing.T) {
	ctx := middleware.WithUID(context.Background(), 7)
	scoutStore := scoutRepo.NewMemory()
	jobs := jobUC.New(jobRepo.NewMemory())
	svcCtx := &svc.ServiceContext{Scout: scoutUC.New(scoutStore), Jobs: jobs}
	data, err := NewRunScanLogic(ctx, svcCtx).RunScan(&types.ScoutScanReq{Brief: types.ScoutBriefPublic{
		Intent: "市集", Mode: domainModeActivity, ThemeKey: "same-theme", ThemeLabel: "週末市集",
		ScanTerms: []string{"市集"}, TargetCount: 12,
	}})
	if err != nil {
		t.Fatal(err)
	}
	if data.Run.Id == "" || data.Job.Id == "" || data.Run.Id == data.Job.Id {
		t.Fatalf("missing independent ids: %+v", data)
	}
	if data.Job.RefId != data.Run.Id || data.Run.JobId != data.Job.Id || data.Run.Status != scoutDomain.RunQueued {
		t.Fatalf("job/run link mismatch: job=%+v run=%+v", data.Job, data.Run)
	}
	var raw map[string]json.RawMessage
	if err := json.Unmarshal([]byte(data.Job.Payload), &raw); err != nil {
		t.Fatal(err)
	}
	var payloadRunID string
	if err := json.Unmarshal(raw["run_id"], &payloadRunID); err != nil || payloadRunID != data.Run.Id {
		t.Fatalf("payload run_id=%q err=%v payload=%s", payloadRunID, err, data.Job.Payload)
	}
	var brief scoutDomain.RunBrief
	if err := json.Unmarshal([]byte(data.Job.Payload), &brief); err != nil || brief.Intent != "市集" || len(brief.ScanTerms) != 1 {
		t.Fatalf("flat worker brief=%+v err=%v", brief, err)
	}
	if data.Run.TargetCount != 12 {
		t.Fatalf("target count=%d", data.Run.TargetCount)
	}
	runs, err := scoutStore.ListRuns(context.Background(), scoutDomain.RunFilter{OwnerUID: 7, Page: 1, PageSize: 10})
	if err != nil || len(runs.Items) != 1 || runs.Items[0].ID != data.Run.Id {
		t.Fatalf("stored runs=%+v err=%v", runs, err)
	}
}

func TestRunScanCreatesFreshRunForSameTheme(t *testing.T) {
	ctx := middleware.WithUID(context.Background(), 7)
	scoutStore := scoutRepo.NewMemory()
	svcCtx := &svc.ServiceContext{Scout: scoutUC.New(scoutStore), Jobs: jobUC.New(jobRepo.NewMemory())}
	logic := NewRunScanLogic(ctx, svcCtx)
	request := &types.ScoutScanReq{Brief: types.ScoutBriefPublic{Intent: "市集", Mode: domainModeActivity, ThemeKey: "repeat", ScanTerms: []string{"市集"}}}
	first, err := logic.RunScan(request)
	if err != nil {
		t.Fatal(err)
	}
	second, err := logic.RunScan(request)
	if err != nil {
		t.Fatal(err)
	}
	if first.Run.Id == second.Run.Id || first.Job.Id == second.Job.Id {
		t.Fatalf("same theme reused id: first=%+v second=%+v", first, second)
	}
}

type failingInsertJobRepo struct{ jobDomain.Repository }

func (failingInsertJobRepo) Insert(context.Context, *jobDomain.Job) error {
	return errors.New("insert unavailable")
}

func TestRunScanSchedulingFailureFailsQueuedRun(t *testing.T) {
	ctx := middleware.WithUID(context.Background(), 7)
	scoutStore := scoutRepo.NewMemory()
	failingJobs := jobUC.New(failingInsertJobRepo{Repository: jobRepo.NewMemory()})
	svcCtx := &svc.ServiceContext{Scout: scoutUC.New(scoutStore), Jobs: failingJobs}
	_, err := NewRunScanLogic(ctx, svcCtx).RunScan(&types.ScoutScanReq{Brief: types.ScoutBriefPublic{
		Intent: "市集", Mode: domainModeActivity, ScanTerms: []string{"市集"},
	}})
	if err == nil {
		t.Fatal("expected scheduling failure")
	}
	runs, listErr := scoutStore.ListRuns(context.Background(), scoutDomain.RunFilter{OwnerUID: 7, Page: 1, PageSize: 10})
	if listErr != nil || len(runs.Items) != 1 || runs.Items[0].Status != scoutDomain.RunFailed {
		t.Fatalf("failed run convergence: runs=%+v err=%v listErr=%v", runs, err, listErr)
	}
	if runs.Items[0].Error == "" {
		t.Fatal("failed run missing safe error")
	}
}

const domainModeActivity = "activity"
