package handlers

import (
	"errors"
	"strings"
	"testing"
	"time"
	"wa-assistant/backend/models"
	"wa-assistant/backend/services"
)

func TestWebTrainingPersistsEvidenceAndReportsPartialFailure(t *testing.T) {
	db := workflowTestDB(t)
	db.AutoMigrate(&models.CrawlJob{}, &models.CrawlPage{})
	job := models.CrawlJob{AgentID: 1, Status: "done"}
	db.Create(&job)
	pages := []models.CrawlPage{
		{AgentID: 1, JobID: job.ID, URL: "https://example.com/a", Title: "a", Status: "crawled", Content: "Toko buka Senin sampai Jumat. Pelanggan dapat menghubungi CS pada jam operasional.", CharCount: 79},
		{AgentID: 1, JobID: job.ID, URL: "https://example.com/b", Title: "b", Status: "crawled", Content: "Toko buka Senin sampai Jumat. Pelanggan dapat menghubungi CS pada jam operasional.", CharCount: 79},
		{AgentID: 1, JobID: job.ID, URL: "https://example.com/c", Title: "c", Status: "crawled", Content: "Sumber berbeda dengan informasi layanan pelanggan yang cukup lengkap.", CharCount: 67},
	}
	db.Create(&pages)
	ids := []uint{pages[0].ID, pages[1].ID, pages[2].ID}
	if err := claimCrawlTraining(1, job.ID, ids); err != nil {
		t.Fatal(err)
	}
	runWebTrainingUsing(1, job.ID, ids, 0, false, func(title, content string) ([]services.QAPair, error) {
		if title == "c" {
			return nil, errors.New("provider_credit_exhausted")
		}
		return []services.QAPair{{Question: "Kapan toko buka?", Answer: "Toko buka Senin sampai Jumat.", Tags: "jam", Evidence: content}}, nil
	})
	var facts []models.Knowledge
	db.Find(&facts)
	if len(facts) != 1 || facts[0].Active || facts[0].ReviewStatus != "draft" || facts[0].SourceExcerpt != pages[0].Content {
		t.Fatalf("draf duplikat/aktif atau bukti hilang: %+v", facts)
	}
	db.Order("id").Find(&pages)
	db.First(&job, job.ID)
	if pages[0].Status != "trained" || pages[1].Status != "trained" || pages[2].Status != "failed" || !strings.Contains(pages[2].Error, "Saldo OpenRouter") {
		t.Fatalf("hasil per halaman salah: %+v", pages)
	}
	if job.Status != "done" || withCrawlDraftCount(job).DraftCount != 1 {
		t.Fatalf("jumlah/status hasil parsial salah: %+v", job)
	}
	if len(services.KnowledgeFor(1)) != 0 {
		t.Fatal("hasil penyusunan dipakai sebelum ditinjau")
	}
}

func TestCrawlDraftCountUsesSavedDraftsAndSourceScope(t *testing.T) {
	db := workflowTestDB(t)
	db.AutoMigrate(&models.CrawlJob{}, &models.CrawlPage{})
	job := models.CrawlJob{AgentID: 1, Status: "done", CreatedAt: time.Now().Add(-time.Hour)}
	db.Create(&job)
	db.Create(&models.CrawlPage{AgentID: 1, JobID: job.ID, URL: "https://example.com/help", Status: "trained"})
	rows := []models.Knowledge{
		{AgentID: 1, Source: "web", SourceURL: "https://example.com/help", ReviewStatus: "draft"},
		{AgentID: 1, Source: "web", SourceURL: "https://example.com/help", ReviewStatus: "published"},
		{AgentID: 2, Source: "web", SourceURL: "https://example.com/help", ReviewStatus: "draft"},
		{AgentID: 1, Source: "web", SourceURL: "https://other.example.com", ReviewStatus: "draft"},
		{AgentID: 1, Source: "web", SourceURL: "https://example.com/help", ReviewStatus: "draft", CreatedAt: job.CreatedAt.Add(-time.Hour)},
	}
	db.Create(&rows)
	if got := withCrawlDraftCount(job).DraftCount; got != 1 {
		t.Fatalf("jumlah draf salah atau bocor lintas sumber: %d", got)
	}
}

func TestFinishCrawlTrainingPreservesPreparedPages(t *testing.T) {
	db := workflowTestDB(t)
	db.AutoMigrate(&models.CrawlJob{}, &models.CrawlPage{})
	job := models.CrawlJob{AgentID: 1, Status: "stopping"}
	db.Create(&job)
	pages := []models.CrawlPage{{AgentID: 1, JobID: job.ID, Status: "trained"}, {AgentID: 1, JobID: job.ID, Status: "queued"}}
	db.Create(&pages)
	finishCrawlTraining(job.ID, "done", "")
	db.Order("id").Find(&pages)
	db.First(&job, job.ID)
	if pages[0].Status != "trained" || pages[1].Status != "crawled" || job.FinishedAt == nil {
		t.Fatalf("stop tidak menyisakan halaman yang dapat dilanjutkan: %+v", pages)
	}
}

func TestClaimCrawlTrainingReservesOnlySelectedPages(t *testing.T) {
	db := workflowTestDB(t)
	if err := db.AutoMigrate(&models.CrawlJob{}, &models.CrawlPage{}); err != nil {
		t.Fatal(err)
	}
	job := models.CrawlJob{AgentID: 1, Status: "done"}
	db.Create(&job)
	pages := []models.CrawlPage{{AgentID: 1, JobID: job.ID, Status: "crawled", CharCount: 59, Content: "Sumber halaman satu berisi informasi lengkap untuk pelanggan."}, {AgentID: 1, JobID: job.ID, Status: "crawled", CharCount: 59, Content: "Sumber halaman dua berisi informasi lengkap untuk pelanggan."}}
	db.Create(&pages)
	if err := claimCrawlTraining(1, job.ID, []uint{pages[0].ID}); err != nil {
		t.Fatal(err)
	}
	if err := claimCrawlTraining(1, job.ID, []uint{pages[0].ID}); err == nil {
		t.Fatal("klik ganda membuat dua pekerja")
	}
	db.First(&pages[0], pages[0].ID)
	db.First(&pages[1], pages[1].ID)
	if pages[0].Status != "queued" || pages[1].Status != "crawled" {
		t.Fatalf("antrean tidak sesuai pilihan: %+v", pages)
	}
}

func TestClaimCrawlTrainingRejectsBusyAndForeignPages(t *testing.T) {
	db := workflowTestDB(t)
	db.AutoMigrate(&models.CrawlJob{}, &models.CrawlPage{})
	job := models.CrawlJob{AgentID: 1, Status: "done"}
	db.Create(&job)
	page := models.CrawlPage{AgentID: 1, JobID: job.ID, Status: "crawled", CharCount: 20}
	db.Create(&page)
	for _, status := range []string{"pending", "crawling", "training", "stopping"} {
		db.Model(&job).Update("status", status)
		if err := claimCrawlTraining(1, job.ID, []uint{page.ID}); err == nil {
			t.Errorf("status %s diterima", status)
		}
	}
	db.Model(&job).Update("status", "done")
	if err := claimCrawlTraining(1, job.ID, []uint{page.ID, 99999}); err == nil {
		t.Fatal("halaman asing diterima")
	}
	db.First(&job, job.ID)
	if job.Status != "done" {
		t.Fatal("validasi gagal tetap mengunci pekerjaan")
	}
}

func TestCrawlLegacyLoadingPagesCannotConsumeAICredit(t *testing.T) {
	db := workflowTestDB(t)
	if err := db.AutoMigrate(&models.CrawlJob{}, &models.CrawlPage{}); err != nil {
		t.Fatal(err)
	}
	job := models.CrawlJob{AgentID: 1, Status: "done"}
	db.Create(&job)
	pages := []models.CrawlPage{
		{AgentID: 1, JobID: job.ID, Status: "crawled", CharCount: 12, Content: "Memuat toko…", Recommended: true},
		// Even an old or incorrect character count cannot bypass content checks.
		{AgentID: 1, JobID: job.ID, Status: "failed", CharCount: 800, Content: "Loading products...", Recommended: true},
		{AgentID: 1, JobID: job.ID, Status: "crawled", CharCount: 500, Content: ""},
	}
	db.Create(&pages)
	for _, page := range pages {
		if err := claimCrawlTraining(1, job.ID, []uint{page.ID}); !errors.Is(err, errCrawlPages) {
			t.Fatalf("unusable page accepted: %d err=%v", page.ID, err)
		}
	}
	for _, page := range crawlPagesOf(job.ID) {
		if page.CanTrain || page.Recommended || page.ContentIssue == "" {
			t.Fatalf("UI eligibility incorrect: %+v", page)
		}
	}
	db.First(&job, job.ID)
	if job.Status != "done" {
		t.Fatal("failed validation claimed job")
	}
	// Protect jobs queued before the fix as well as new requests.
	db.Model(&pages[0]).Update("status", "queued")
	calls := 0
	runWebTrainingUsing(1, job.ID, []uint{pages[0].ID}, 0, false, func(string, string) ([]services.QAPair, error) {
		calls++
		return nil, nil
	})
	db.First(&pages[0], pages[0].ID)
	if calls != 0 || pages[0].Status != "failed" {
		t.Fatalf("provider calls=%d status=%s", calls, pages[0].Status)
	}
}

func TestCrawlValidSourceCanRetryAIError(t *testing.T) {
	db := workflowTestDB(t)
	db.AutoMigrate(&models.CrawlJob{}, &models.CrawlPage{})
	job := models.CrawlJob{AgentID: 1, Status: "done"}
	db.Create(&job)
	content := "Rak sepatu Capella tersedia dengan harga Rp694.000. Tautan produk tersedia di katalog resmi toko."
	page := models.CrawlPage{AgentID: 1, JobID: job.ID, Status: "failed", CharCount: len([]rune(content)), Content: content, Error: "Saldo AI habis"}
	db.Create(&page)
	presented := crawlPagesOf(job.ID)
	if len(presented) != 1 || !presented[0].CanTrain || presented[0].ContentPreview != content {
		t.Fatalf("valid source cannot be retried or inspected: %+v", presented)
	}
	if err := claimCrawlTraining(1, job.ID, []uint{page.ID}); err != nil {
		t.Fatal(err)
	}
}
