package orchestrator

import (
	"context"
	"errors"
	"sync"
	"testing"

	"github.com/libnovel/backend/internal/domain"
)

// ── stubs ─────────────────────────────────────────────────────────────────────

type stubScraper struct {
	meta     domain.BookMeta
	metaErr  error
	refs     []domain.ChapterRef
	refsErr  error
	chapters map[int]domain.Chapter
	chapErr  map[int]error
}

func (s *stubScraper) SourceName() string { return "stub" }

func (s *stubScraper) ScrapeCatalogue(ctx context.Context) (<-chan domain.CatalogueEntry, <-chan error) {
	ch := make(chan domain.CatalogueEntry)
	errs := make(chan error)
	close(ch)
	close(errs)
	return ch, errs
}

func (s *stubScraper) ScrapeMetadata(_ context.Context, _ string) (domain.BookMeta, error) {
	return s.meta, s.metaErr
}

func (s *stubScraper) ScrapeChapterList(_ context.Context, _ string, _ int) ([]domain.ChapterRef, error) {
	return s.refs, s.refsErr
}

func (s *stubScraper) ScrapeChapterText(_ context.Context, ref domain.ChapterRef) (domain.Chapter, error) {
	if s.chapErr != nil {
		if err, ok := s.chapErr[ref.Number]; ok {
			return domain.Chapter{}, err
		}
	}
	if s.chapters != nil {
		if ch, ok := s.chapters[ref.Number]; ok {
			return ch, nil
		}
	}
	return domain.Chapter{Ref: ref, Text: "text"}, nil
}

func (s *stubScraper) ScrapeRanking(ctx context.Context, maxPages int) (<-chan domain.BookMeta, <-chan error) {
	ch := make(chan domain.BookMeta)
	errs := make(chan error)
	close(ch)
	close(errs)
	return ch, errs
}

type stubStore struct {
	mu              sync.Mutex
	metaWritten     []domain.BookMeta
	chaptersWritten []domain.Chapter
	existing        map[string]bool // "slug:N" → exists
	writeMetaErr    error
}

func (s *stubStore) WriteMetadata(_ context.Context, meta domain.BookMeta) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.writeMetaErr != nil {
		return s.writeMetaErr
	}
	s.metaWritten = append(s.metaWritten, meta)
	return nil
}

func (s *stubStore) WriteChapter(_ context.Context, slug string, ch domain.Chapter) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.chaptersWritten = append(s.chaptersWritten, ch)
	return nil
}

func (s *stubStore) WriteChapterRefs(_ context.Context, _ string, _ []domain.ChapterRef) error {
	return nil
}

func (s *stubStore) DeduplicateChapters(_ context.Context, _ string) (int, error) { return 0, nil }

func (s *stubStore) ChapterExists(_ context.Context, slug string, ref domain.ChapterRef) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	key := slug + ":" + string(rune('0'+ref.Number))
	return s.existing[key]
}

// ── tests ──────────────────────────────────────────────────────────────────────

func TestRunBook_HappyPath(t *testing.T) {
	sc := &stubScraper{
		meta: domain.BookMeta{Slug: "test-book", Title: "Test Book", SourceURL: "https://example.com/book/test-book"},
		refs: []domain.ChapterRef{
			{Number: 1, Title: "Ch 1", URL: "https://example.com/book/test-book/chapter-1"},
			{Number: 2, Title: "Ch 2", URL: "https://example.com/book/test-book/chapter-2"},
			{Number: 3, Title: "Ch 3", URL: "https://example.com/book/test-book/chapter-3"},
		},
	}
	st := &stubStore{}
	o := New(Config{Workers: 2}, sc, st, nil)

	task := domain.ScrapeTask{
		ID:        "t1",
		Kind:      "book",
		TargetURL: "https://example.com/book/test-book",
	}

	result := o.RunBook(context.Background(), task)

	if result.ErrorMessage != "" {
		t.Fatalf("unexpected error: %s", result.ErrorMessage)
	}
	if result.BooksFound != 1 {
		t.Errorf("BooksFound = %d, want 1", result.BooksFound)
	}
	if result.ChaptersScraped != 3 {
		t.Errorf("ChaptersScraped = %d, want 3", result.ChaptersScraped)
	}
}

func TestRunBook_MetadataError(t *testing.T) {
	sc := &stubScraper{metaErr: errors.New("404 not found")}
	st := &stubStore{}
	o := New(Config{Workers: 1}, sc, st, nil)

	result := o.RunBook(context.Background(), domain.ScrapeTask{
		ID:        "t2",
		TargetURL: "https://example.com/book/missing",
	})

	if result.ErrorMessage == "" {
		t.Fatal("expected ErrorMessage to be set")
	}
	if result.Errors != 1 {
		t.Errorf("Errors = %d, want 1", result.Errors)
	}
}

func TestRunBook_ChapterRange(t *testing.T) {
	sc := &stubScraper{
		meta: domain.BookMeta{Slug: "range-book", SourceURL: "https://example.com/book/range-book"},
		refs: func() []domain.ChapterRef {
			var refs []domain.ChapterRef
			for i := 1; i <= 10; i++ {
				refs = append(refs, domain.ChapterRef{Number: i, URL: "https://example.com/book/range-book/chapter-" + string(rune('0'+i))})
			}
			return refs
		}(),
	}
	st := &stubStore{}
	o := New(Config{Workers: 2}, sc, st, nil)

	result := o.RunBook(context.Background(), domain.ScrapeTask{
		ID:          "t3",
		TargetURL:   "https://example.com/book/range-book",
		FromChapter: 3,
		ToChapter:   7,
	})

	if result.ErrorMessage != "" {
		t.Fatalf("unexpected error: %s", result.ErrorMessage)
	}
	// chapters 3–7 = 5 scraped, chapters 1-2 and 8-10 = 5 skipped
	if result.ChaptersScraped != 5 {
		t.Errorf("ChaptersScraped = %d, want 5", result.ChaptersScraped)
	}
	if result.ChaptersSkipped != 5 {
		t.Errorf("ChaptersSkipped = %d, want 5", result.ChaptersSkipped)
	}
}

func TestRunBook_ContextCancellation(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	sc := &stubScraper{
		meta: domain.BookMeta{Slug: "ctx-book", SourceURL: "https://example.com/book/ctx-book"},
		refs: []domain.ChapterRef{
			{Number: 1, URL: "https://example.com/book/ctx-book/chapter-1"},
		},
	}
	st := &stubStore{}
	o := New(Config{Workers: 1}, sc, st, nil)

	// Should not panic; result may have errors or zero chapters.
	result := o.RunBook(ctx, domain.ScrapeTask{
		ID:        "t4",
		TargetURL: "https://example.com/book/ctx-book",
	})
	_ = result
}

func TestRunBook_EmptyTargetURL(t *testing.T) {
	o := New(Config{Workers: 1}, &stubScraper{}, &stubStore{}, nil)
	result := o.RunBook(context.Background(), domain.ScrapeTask{ID: "t5"})
	if result.ErrorMessage == "" {
		t.Fatal("expected ErrorMessage for empty target URL")
	}
}
