import pytest

import app.services.catalog.build as build


class Recorder:
    def __init__(self):
        self.calls = []

    def __call__(self, percent, step):
        self.calls.append((percent, step))

    @property
    def percents(self):
        return [p for p, _ in self.calls]


@pytest.fixture
def stubbed(monkeypatch):
    order = []

    def enrich(tenant_id, force=False, progress=None):
        order.append("enrich")
        if progress:
            progress(50, "extracting attributes — batch 5 of 11")
            progress(100, "extracting attributes — batch 11 of 11")
        return {"products": 218, "extracted": 218}

    def pair(tenant_id, progress=None):
        order.append("pair")
        if progress:
            progress(50, "scoring candidate pairs")
            progress(100, "writing pairing graph")
        return {"products": 212, "pairs": 1247}

    monkeypatch.setattr(build, "enrich_tenant", enrich)
    monkeypatch.setattr(build, "pair_tenant", pair)
    monkeypatch.setattr(build, "get_sources", lambda t: [])
    return order


def test_the_stages_run_in_order(stubbed):
    # Pairing reads what enrichment writes: is_accessory is what makes a phone
    # suggest a case. Pairing first would produce a graph with no complements
    # worth having.
    build.build_catalog("org_x")
    assert stubbed == ["enrich", "pair"]


def test_the_report_carries_every_stage(stubbed):
    report = build.build_catalog("org_x")
    assert set(report["stages"]) == {"sync", "enrich", "pair"}
    assert report["stages"]["enrich"]["extracted"] == 218
    assert report["stages"]["pair"]["pairs"] == 1247


def test_the_headline_numbers_come_from_pairing(stubbed):
    report = build.build_catalog("org_x")
    assert report["products"] == 212
    assert report["pairs"] == 1247


def test_progress_is_one_continuous_bar(stubbed):
    # Three stages, one 0-100. A bar that restarts per stage tells a merchant
    # the work is going backwards.
    recorder = Recorder()
    build.build_catalog("org_x", progress=recorder)
    assert recorder.percents == sorted(recorder.percents)
    assert recorder.percents[-1] == 100
    assert all(0 <= p <= 100 for p in recorder.percents)


def test_each_stage_reports_inside_its_own_slice(stubbed):
    recorder = Recorder()
    build.build_catalog("org_x", progress=recorder)

    steps = dict((step, percent) for percent, step in recorder.calls)
    assert 10 <= steps["extracting attributes — batch 5 of 11"] <= 60
    assert 60 <= steps["scoring candidate pairs"] <= 100


def test_a_csv_only_tenant_skips_sync(stubbed):
    # Nothing to fetch. Failing here would break the one button this exists for.
    report = build.build_catalog("org_x")
    assert "skipped" in report["stages"]["sync"]
    assert stubbed == ["enrich", "pair"]


def test_a_failing_stage_stops_the_build(monkeypatch, stubbed):
    def boom(tenant_id, force=False, progress=None):
        raise RuntimeError("model unavailable")

    monkeypatch.setattr(build, "enrich_tenant", boom)

    with pytest.raises(RuntimeError):
        build.build_catalog("org_x")
    # Pairing an un-enriched catalogue is worse than not pairing at all.
    assert "pair" not in stubbed


def test_force_is_passed_to_enrichment(monkeypatch, stubbed):
    seen = {}

    def enrich(tenant_id, force=False, progress=None):
        seen["force"] = force
        return {}

    monkeypatch.setattr(build, "enrich_tenant", enrich)
    build.build_catalog("org_x", force=True)
    assert seen["force"] is True


def test_it_works_with_no_progress_callback(stubbed):
    assert build.build_catalog("org_x")["pairs"] == 1247
