from __future__ import annotations

import csv
import json
from pathlib import Path
import sys
import types

import pytest
from typer.testing import CliRunner

from worai.commands import structured_data as structured_data_cmd
from worai.core.structured_data_inventory import (
    InventoryOptions,
    InventoryRow,
    build_inventory,
    run_inventory,
)
from worai.errors import UsageError


def _load_json_from_cli_output(output: str) -> dict[str, object]:
    start = output.find("{")
    if start < 0:
        raise AssertionError(f"No JSON payload found in output: {output!r}")
    return json.loads(output[start:])


def _install_fake_sdk_ingestion(monkeypatch: pytest.MonkeyPatch, callback) -> None:
    pkg = types.ModuleType("wordlift_sdk")
    mod = types.ModuleType("wordlift_sdk.ingestion")
    mod.create_structured_data_inventory_from_ingestion = callback
    pkg.ingestion = mod
    monkeypatch.setitem(sys.modules, "wordlift_sdk", pkg)
    monkeypatch.setitem(sys.modules, "wordlift_sdk.ingestion", mod)


def test_build_inventory_calls_sdk_with_urls_bundle(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    seen: dict[str, object] = {}

    class _FakeDataFrame:
        def to_dict(self, orient: str = "records"):
            assert orient == "records"
            return [
                {
                    "url": "https://example.com/a",
                    "faq_markup": "yes",
                    "faq_markup_from_graph": "no",
                    "types": "WebPage",
                    "structured_data": "{}",
                }
            ]

    def _fake_create(**kwargs):
        seen["kwargs"] = kwargs
        return _FakeDataFrame()

    _install_fake_sdk_ingestion(monkeypatch, _fake_create)

    source = tmp_path / "urls.txt"
    source.write_text("https://example.com/a\nhttps://example.com/b\n", encoding="utf-8")

    rows = build_inventory(
        InventoryOptions(
            source=str(source),
            api_key="key",
            output=str(tmp_path / "out.csv"),
            ingest_source="urls",
            ingest_loader="playwright",
            ingest_passthrough_when_html=False,
            ingest_timeout_ms=10000,
            ingest_retry_attempts=1,
            ingest_retry_backoff_ms=0,
            playwright_wait_until="domcontentloaded",
            url_regex=r"/a$",
        )
    )

    assert len(rows) == 1
    assert rows[0].url == "https://example.com/a"
    kwargs = seen["kwargs"]
    assert kwargs["api_key"] == "key"
    assert kwargs["base_url"] == "https://api.wordlift.io"
    assert kwargs["output_csv"] == str(tmp_path / "out.csv")
    bundle = kwargs["source_bundle"]
    assert bundle["INGEST_SOURCE"] == "urls"
    assert bundle["INGEST_LOADER"] == "playwright"
    assert bundle["INGEST_PASSTHROUGH_WHEN_HTML"] is False
    assert bundle["INGEST_TIMEOUT_MS"] == 10000
    assert bundle["INGEST_RETRY_ATTEMPTS"] == 1
    assert bundle["INGEST_RETRY_BACKOFF_MS"] == 0
    assert bundle["PLAYWRIGHT_WAIT_UNTIL"] == "domcontentloaded"
    assert bundle["URL_REGEX"] == r"/a$"
    assert bundle["URLS"] == ["https://example.com/a", "https://example.com/b"]


def test_build_inventory_auto_infers_sitemap(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    seen: dict[str, object] = {}

    class _FakeDataFrame:
        def to_dict(self, orient: str = "records"):
            return []

    def _fake_create(**kwargs):
        seen["kwargs"] = kwargs
        return _FakeDataFrame()

    _install_fake_sdk_ingestion(monkeypatch, _fake_create)

    rows = build_inventory(
        InventoryOptions(
            source="https://example.com/sitemap.xml",
            api_key="key",
            output=str(tmp_path / "out.csv"),
        )
    )
    assert rows == []
    bundle = seen["kwargs"]["source_bundle"]
    assert bundle["INGEST_SOURCE"] == "sitemap"
    assert bundle["INGEST_LOADER"] == "web_scrape_api"
    assert "INGEST_TIMEOUT_MS" not in bundle
    assert "PLAYWRIGHT_WAIT_UNTIL" not in bundle
    assert bundle["SITEMAP_URL"] == "https://example.com/sitemap.xml"


def test_build_inventory_source_type_debug_cloud_maps_to_local(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    seen: dict[str, object] = {}

    class _FakeDataFrame:
        def to_dict(self, orient: str = "records"):
            return []

    def _fake_create(**kwargs):
        seen["kwargs"] = kwargs
        return _FakeDataFrame()

    _install_fake_sdk_ingestion(monkeypatch, _fake_create)

    source = tmp_path / "cloud"
    source.mkdir()

    build_inventory(
        InventoryOptions(
            source=str(source),
            source_type="debug-cloud",
            api_key="key",
            output=str(tmp_path / "out.csv"),
        )
    )

    bundle = seen["kwargs"]["source_bundle"]
    assert bundle["INGEST_SOURCE"] == "local"
    assert bundle["INGEST_LOCAL_ITEMS"] == []


def test_build_inventory_requires_sheet_name_for_sheets_source(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    _install_fake_sdk_ingestion(monkeypatch, lambda **_kwargs: None)

    with pytest.raises(UsageError, match="--sheet-name is required"):
        build_inventory(
            InventoryOptions(
                source="https://docs.google.com/spreadsheets/d/12345678901234567890/edit",
                api_key="key",
                output=str(tmp_path / "out.csv"),
                ingest_source="sheets",
            )
        )


def test_build_inventory_reads_csv_when_sdk_returns_none(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    def _fake_create(**kwargs):
        with open(kwargs["output_csv"], "w", encoding="utf-8", newline="") as handle:
            writer = csv.DictWriter(
                handle,
                fieldnames=["url", "faq_markup", "faq_markup_from_graph", "types", "structured_data"],
            )
            writer.writeheader()
            writer.writerow(
                {
                    "url": "https://example.com/a",
                    "faq_markup": "yes",
                    "faq_markup_from_graph": "yes",
                    "types": "WebPage",
                    "structured_data": "{}",
                }
            )
        return None

    _install_fake_sdk_ingestion(monkeypatch, _fake_create)

    rows = build_inventory(
        InventoryOptions(
            source="https://example.com/sitemap.xml",
            api_key="key",
            output=str(tmp_path / "out.csv"),
        )
    )

    assert len(rows) == 1
    assert rows[0].faq_markup == "yes"


def test_run_inventory_requires_exactly_one_destination() -> None:
    options = InventoryOptions(
        source="https://example.com/sitemap.xml",
        api_key="key",
    )
    with pytest.raises(UsageError):
        run_inventory(options)

    options = InventoryOptions(
        source="https://example.com/sitemap.xml",
        api_key="key",
        output="out.csv",
        destination_spreadsheet_id="spreadsheet-id",
    )
    with pytest.raises(UsageError):
        run_inventory(options)


def test_run_inventory_requires_destination_sheet_name() -> None:
    options = InventoryOptions(
        source="https://example.com/sitemap.xml",
        api_key="key",
        destination_spreadsheet_id="spreadsheet-id",
    )
    with pytest.raises(UsageError):
        run_inventory(options)


def test_run_inventory_writes_csv(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    rows = [
        InventoryRow(
            url="https://example.com/a",
            faq_markup="no",
            faq_markup_from_graph="no",
            types="WebPage",
            structured_data='{"@context":"https://schema.org","@graph":[]}',
        )
    ]

    monkeypatch.setattr(
        "worai.core.structured_data_inventory.build_inventory",
        lambda _options, on_progress=None: rows,
    )

    output_path = tmp_path / "inventory.csv"
    run_inventory(
        InventoryOptions(
            source="https://example.com/sitemap.xml",
            api_key="key",
            output=str(output_path),
        )
    )

    with output_path.open("r", encoding="utf-8") as handle:
        reader = csv.DictReader(handle)
        records = list(reader)

    assert len(records) == 1
    assert records[0]["url"] == "https://example.com/a"
    assert records[0]["types"] == "WebPage"


def test_inventory_command_builds_options_and_emits_summary(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, object] = {}

    def _fake_run_inventory(options, on_progress=None):
        assert callable(on_progress)
        captured["options"] = options
        return []

    monkeypatch.setattr(structured_data_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("key", explicit_profile or "default", "worai.toml"))
    monkeypatch.setattr(structured_data_cmd, "run_inventory", _fake_run_inventory)

    runner = CliRunner()
    result = runner.invoke(
        structured_data_cmd.app,
        ["inventory", "https://example.com/sitemap.xml", "--output", "report.csv"],
    )

    assert result.exit_code == 0
    payload = _load_json_from_cli_output(result.output)
    assert payload["total"] == 0
    options = captured["options"]
    assert isinstance(options, InventoryOptions)
    assert options.source == "https://example.com/sitemap.xml"
    assert options.output == "report.csv"
    assert options.concurrency == "auto"


def test_inventory_command_sets_destination_sheet_options(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, object] = {}

    def _fake_run_inventory(options, on_progress=None):
        assert callable(on_progress)
        captured["options"] = options
        return []

    monkeypatch.setattr(structured_data_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("key", explicit_profile or "default", "worai.toml"))
    monkeypatch.setattr(structured_data_cmd, "run_inventory", _fake_run_inventory)

    runner = CliRunner()
    result = runner.invoke(
        structured_data_cmd.app,
        [
            "inventory",
            "https://example.com/sitemap.xml",
            "--destination-sheet-id",
            "spreadsheet-id",
            "--destination-sheet-name",
            "Inventory",
        ],
    )

    assert result.exit_code == 0
    options = captured["options"]
    assert isinstance(options, InventoryOptions)
    assert options.destination_spreadsheet_id == "spreadsheet-id"
    assert options.destination_sheet_name == "Inventory"


def test_inventory_command_sets_concurrency(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, object] = {}

    def _fake_run_inventory(options, on_progress=None):
        assert callable(on_progress)
        captured["options"] = options
        return []

    monkeypatch.setattr(structured_data_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("key", explicit_profile or "default", "worai.toml"))
    monkeypatch.setattr(structured_data_cmd, "run_inventory", _fake_run_inventory)

    runner = CliRunner()
    result = runner.invoke(
        structured_data_cmd.app,
        [
            "inventory",
            "https://example.com/sitemap.xml",
            "--output",
            "report.csv",
            "--concurrency",
            "3",
        ],
    )

    assert result.exit_code == 0
    options = captured["options"]
    assert isinstance(options, InventoryOptions)
    assert options.concurrency == "3"


def test_inventory_command_sets_source_type(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, object] = {}

    def _fake_run_inventory(options, on_progress=None):
        assert callable(on_progress)
        captured["options"] = options
        return []

    monkeypatch.setattr(structured_data_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("key", explicit_profile or "default", "worai.toml"))
    monkeypatch.setattr(structured_data_cmd, "run_inventory", _fake_run_inventory)

    runner = CliRunner()
    result = runner.invoke(
        structured_data_cmd.app,
        [
            "inventory",
            "/tmp/debug_cloud",
            "--output",
            "report.csv",
            "--source-type",
            "debug-cloud",
        ],
    )

    assert result.exit_code == 0
    options = captured["options"]
    assert isinstance(options, InventoryOptions)
    assert options.source_type == "debug-cloud"


def test_inventory_command_sets_new_ingest_flags(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, object] = {}

    def _fake_run_inventory(options, on_progress=None):
        assert callable(on_progress)
        captured["options"] = options
        return []

    monkeypatch.setattr(structured_data_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("key", explicit_profile or "default", "worai.toml"))
    monkeypatch.setattr(structured_data_cmd, "run_inventory", _fake_run_inventory)

    runner = CliRunner()
    result = runner.invoke(
        structured_data_cmd.app,
        [
            "inventory",
            "https://example.com/sitemap.xml",
            "--output",
            "report.csv",
            "--ingest-source",
            "sitemap",
            "--ingest-loader",
            "web_scrape_api",
            "--url-regex",
            "/blog/",
            "--no-ingest-passthrough-when-html",
        ],
    )

    assert result.exit_code == 0
    payload = _load_json_from_cli_output(result.output)
    assert payload["url_regex"] == "/blog/"
    options = captured["options"]
    assert isinstance(options, InventoryOptions)
    assert options.ingest_source == "sitemap"
    assert options.ingest_loader == "web_scrape_api"
    assert options.url_regex == "/blog/"
    assert options.ingest_passthrough_when_html is False


def test_inventory_progress_callback_lifecycle_and_zero_case() -> None:
    bars: list[object] = []

    class _Bar:
        def __init__(self, *, total, desc, unit, leave):
            self.total = total
            self.desc = desc
            self.unit = unit
            self.leave = leave
            self.updates: list[int] = []
            self.closed = False

        def update(self, delta: int):
            self.updates.append(delta)

        def refresh(self):
            return None

        def close(self):
            self.closed = True

    def _factory(**kwargs):
        bar = _Bar(**kwargs)
        bars.append(bar)
        return bar

    callback, close = structured_data_cmd._build_inventory_progress_callback(
        enabled=True,
        progress_factory=_factory,
    )
    callback({"event": "inventory.progress.started", "meta": {"total": 3}})
    callback({"event": "inventory.progress.updated", "meta": {"completed": 1, "total": 3}})
    callback({"event": "inventory.progress.updated", "meta": {"completed": 1, "total": 3}})
    callback({"event": "inventory.progress.updated", "meta": {"completed": 3, "total": 3}})
    callback({"event": "inventory.progress.completed", "meta": {"completed": 3, "total": 3}})
    close()

    assert len(bars) == 1
    assert bars[0].updates == [1, 2]
    assert bars[0].closed is True

    zero_bars: list[object] = []

    def _zero_factory(**kwargs):
        bar = _Bar(**kwargs)
        zero_bars.append(bar)
        return bar

    callback, close = structured_data_cmd._build_inventory_progress_callback(
        enabled=True,
        progress_factory=_zero_factory,
    )
    callback({"event": "inventory.progress.started", "meta": {"total": 0}})
    callback({"event": "inventory.progress.updated", "meta": {"completed": 0, "total": 0}})
    callback({"event": "inventory.progress.completed", "meta": {"completed": 0, "total": 0}})
    close()

    assert len(zero_bars) == 1
    assert zero_bars[0].updates == []
    assert zero_bars[0].closed is True


def test_inventory_progress_callback_reports_events_when_progress_disabled() -> None:
    seen_events: list[dict[str, object]] = []

    callback, close = structured_data_cmd._build_inventory_progress_callback(
        enabled=False,
        progress_factory=None,
        event_reporter=lambda event: seen_events.append(event),
    )
    callback({"event": "inventory.progress.started", "meta": {"total": 2}})
    callback({"event": "inventory.progress.updated", "meta": {"completed": 1, "total": 2}})
    callback({"event": "inventory.progress.completed", "meta": {"completed": 2, "total": 2}})
    close()

    assert len(seen_events) == 3
    assert seen_events[0]["event"] == "inventory.progress.started"
    assert seen_events[-1]["event"] == "inventory.progress.completed"


def test_inventory_command_resolves_ingest_runtime_settings_from_profile(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    captured: dict[str, object] = {}

    def _fake_run_inventory(options, on_progress=None):
        captured["options"] = options
        return []

    monkeypatch.setattr(
        structured_data_cmd,
        "load_profile_settings",
        lambda _ctx: (
            {
                "ingest.timeout_ms": "9000",
                "ingest.retry_attempts": "2",
                "ingest.retry_backoff_ms": "100",
                "ingest.playwright_wait_until": "domcontentloaded",
            },
            "default",
        ),
    )
    monkeypatch.setattr(
        structured_data_cmd,
        "resolve_profile_api_key",
        lambda _ctx, explicit_profile=None: ("key", explicit_profile or "default", "worai.toml"),
    )
    monkeypatch.setattr(structured_data_cmd, "run_inventory", _fake_run_inventory)

    result = CliRunner().invoke(
        structured_data_cmd.app,
        ["inventory", "https://example.com/sitemap.xml", "--output", "report.csv"],
    )

    assert result.exit_code == 0
    options = captured["options"]
    assert isinstance(options, InventoryOptions)
    assert options.ingest_timeout_ms == 9000
    assert options.ingest_retry_attempts == 2
    assert options.ingest_retry_backoff_ms == 100
    assert options.playwright_wait_until == "domcontentloaded"
