from __future__ import annotations

import json
from pathlib import Path
from types import SimpleNamespace

import pytest
from typer.testing import CliRunner

from worai.commands import graph as graph_cmd
from worai.errors import UsageError


class _FakeReport:
    def __init__(self, load_errors: list | None = None, text: str = "OK", data: dict | None = None) -> None:
        self.load_errors = load_errors or []
        self._text = text
        self._data = data or {"summary": "ok"}

    def to_text(self) -> str:
        return self._text

    def to_dict(self) -> dict:
        return self._data


class _FakeAuditor:
    def __init__(self, report: _FakeReport) -> None:
        self._report = report

    def audit(self, path: str, options: object) -> _FakeReport:
        return self._report


def _patch_sdk(monkeypatch, report: _FakeReport | None = None, auditor_cls=None) -> None:
    if report is None:
        report = _FakeReport()
    if auditor_cls is None:
        _auditor = _FakeAuditor(report)

        class _Cls:
            def __call__(self):
                return _auditor

        auditor_cls = _Cls()

    monkeypatch.setattr(graph_cmd, "_GraphAuditor", auditor_cls)
    monkeypatch.setattr(graph_cmd, "_AuditOptions", SimpleNamespace)


# ---------------------------------------------------------------------------
# Basic invocation
# ---------------------------------------------------------------------------


def test_audit_text_output_exit_0(monkeypatch) -> None:
    report = _FakeReport(text="Audit OK\n")
    _patch_sdk(monkeypatch, report)

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl"])

    assert result.exit_code == 0
    assert "Audit OK" in result.output


def test_audit_json_output_exit_0(monkeypatch) -> None:
    report = _FakeReport(data={"kpis": []})
    _patch_sdk(monkeypatch, report)

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl", "--format", "json"])

    assert result.exit_code == 0
    payload = json.loads(result.output)
    assert payload == {"kpis": []}


def test_audit_exit_1_on_load_errors(monkeypatch) -> None:
    report = _FakeReport(load_errors=["parse error"], text="FAIL\n")
    _patch_sdk(monkeypatch, report)

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl"])

    assert result.exit_code == 1
    assert "FAIL" in result.output


# ---------------------------------------------------------------------------
# Option forwarding
# ---------------------------------------------------------------------------


def test_audit_options_forwarded(monkeypatch) -> None:
    seen: dict = {}

    class _CapturingAuditor:
        def audit(self, path, options):
            seen["path"] = path
            seen["options"] = options
            return _FakeReport()

    class _AuditorCls:
        def __call__(self):
            return _CapturingAuditor()

    monkeypatch.setattr(graph_cmd, "_GraphAuditor", _AuditorCls())
    monkeypatch.setattr(graph_cmd, "_AuditOptions", lambda **kw: SimpleNamespace(**kw))

    result = CliRunner().invoke(
        graph_cmd.app,
        [
            "audit",
            "my_graph.ttl",
            "--depth",
            "3",
            "--list-components",
            "--rich-snippets-granularity",
            "entities",
            "--workers",
            "4",
        ],
    )

    assert result.exit_code == 0
    assert seen["path"] == "my_graph.ttl"
    opts = seen["options"]
    assert opts.subgraph_depth == 3
    assert opts.list_isolated_components is True
    assert opts.rich_snippets_granularity == "entities"
    assert opts.max_workers == 4
    assert opts.profile is None


def test_audit_shape_options_forwarded(monkeypatch) -> None:
    seen: dict = {}

    class _Cap:
        def audit(self, path, options):
            seen["options"] = options
            return _FakeReport()

    class _Cls:
        def __call__(self):
            return _Cap()

    monkeypatch.setattr(graph_cmd, "_GraphAuditor", _Cls())
    monkeypatch.setattr(graph_cmd, "_AuditOptions", lambda **kw: SimpleNamespace(**kw))

    result = CliRunner().invoke(
        graph_cmd.app,
        [
            "audit",
            "graph.ttl",
            "--builtin-shape",
            "google-article.ttl",
            "--builtin-shape",
            "google-faq.ttl",
            "--shape",
            "./custom.ttl",
            "--issue-level",
            "error",
        ],
    )

    assert result.exit_code == 0
    opts = seen["options"]
    assert opts.builtin_shapes == ["google-article.ttl", "google-faq.ttl"]
    assert opts.exclude_builtin_shapes is None
    assert opts.extra_shapes == ["./custom.ttl"]
    assert opts.issue_level == "error"


def test_audit_exclude_builtin_shapes_forwarded(monkeypatch) -> None:
    seen: dict = {}

    class _Cap:
        def audit(self, path, options):
            seen["options"] = options
            return _FakeReport()

    class _Cls:
        def __call__(self):
            return _Cap()

    monkeypatch.setattr(graph_cmd, "_GraphAuditor", _Cls())
    monkeypatch.setattr(graph_cmd, "_AuditOptions", lambda **kw: SimpleNamespace(**kw))

    result = CliRunner().invoke(
        graph_cmd.app,
        ["audit", "graph.ttl", "--exclude-builtin-shape", "google-article.ttl"],
    )

    assert result.exit_code == 0
    opts = seen["options"]
    assert opts.builtin_shapes is None
    assert opts.exclude_builtin_shapes == ["google-article.ttl"]


def test_audit_builtin_and_exclude_are_mutually_exclusive(monkeypatch) -> None:
    _patch_sdk(monkeypatch)

    result = CliRunner().invoke(
        graph_cmd.app,
        [
            "audit",
            "graph.ttl",
            "--builtin-shape",
            "a.ttl",
            "--exclude-builtin-shape",
            "b.ttl",
        ],
    )

    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "mutually exclusive" in str(result.exception)


def test_audit_no_shapes_passes_none(monkeypatch) -> None:
    seen: dict = {}

    class _Cap:
        def audit(self, path, options):
            seen["options"] = options
            return _FakeReport()

    class _Cls:
        def __call__(self):
            return _Cap()

    monkeypatch.setattr(graph_cmd, "_GraphAuditor", _Cls())
    monkeypatch.setattr(graph_cmd, "_AuditOptions", lambda **kw: SimpleNamespace(**kw))

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl"])

    assert result.exit_code == 0
    opts = seen["options"]
    assert opts.builtin_shapes is None
    assert opts.exclude_builtin_shapes is None
    assert opts.extra_shapes is None
    assert opts.issue_level == "warning"


# ---------------------------------------------------------------------------
# Profile resolution — global switch
# ---------------------------------------------------------------------------


def test_audit_no_profile_passes_none(monkeypatch) -> None:
    seen: dict = {}

    class _Cap:
        def audit(self, path, options):
            seen["profile"] = options.profile
            return _FakeReport()

    class _Cls:
        def __call__(self):
            return _Cap()

    monkeypatch.setattr(graph_cmd, "_GraphAuditor", _Cls())
    monkeypatch.setattr(graph_cmd, "_AuditOptions", lambda **kw: SimpleNamespace(**kw))

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl"], obj={"profile": None})

    assert result.exit_code == 0
    assert seen["profile"] is None


def test_audit_profile_loaded_from_global_switch(monkeypatch) -> None:
    fake_profile = SimpleNamespace(api_key="wl_test")
    seen: dict = {}

    monkeypatch.setattr(
        graph_cmd,
        "load_profile",
        lambda ctx: (fake_profile, "acme", Path("worai.toml")),
    )

    class _Cap:
        def audit(self, path, options):
            seen["profile"] = options.profile
            return _FakeReport()

    class _Cls:
        def __call__(self):
            return _Cap()

    monkeypatch.setattr(graph_cmd, "_GraphAuditor", _Cls())
    monkeypatch.setattr(graph_cmd, "_AuditOptions", lambda **kw: SimpleNamespace(**kw))

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl"], obj={"profile": "acme"})

    assert result.exit_code == 0
    assert seen["profile"] is fake_profile


def test_audit_profile_load_error_raises_usage_error(monkeypatch) -> None:
    monkeypatch.setattr(
        graph_cmd,
        "load_profile",
        lambda ctx: (_ for _ in ()).throw(ValueError("missing profile")),
    )
    monkeypatch.setattr(graph_cmd, "_GraphAuditor", object())
    monkeypatch.setattr(graph_cmd, "_AuditOptions", SimpleNamespace)

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl"], obj={"profile": "missing"})

    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "missing profile" in str(result.exception)


# ---------------------------------------------------------------------------
# SDK unavailable
# ---------------------------------------------------------------------------


def test_audit_raises_usage_error_when_sdk_unavailable(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "_GraphAuditor", None)
    monkeypatch.setattr(graph_cmd, "_AuditOptions", None)

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl"])

    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "wordlift-sdk>=6.11.0" in str(result.exception)


# ---------------------------------------------------------------------------
# Validation helpers
# ---------------------------------------------------------------------------


def test_normalize_rich_snippets_granularity() -> None:
    assert graph_cmd._normalize_rich_snippets_granularity("counts") == "counts"
    assert graph_cmd._normalize_rich_snippets_granularity("Entities") == "entities"
    with pytest.raises(UsageError, match="counts.*entities"):
        graph_cmd._normalize_rich_snippets_granularity("bad")


def test_audit_invalid_format(monkeypatch) -> None:
    _patch_sdk(monkeypatch)

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl", "--format", "xml"])

    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)


def test_audit_invalid_rich_snippets_granularity(monkeypatch) -> None:
    _patch_sdk(monkeypatch)

    result = CliRunner().invoke(
        graph_cmd.app, ["audit", "graph.ttl", "--rich-snippets-granularity", "bad"]
    )

    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)


# ---------------------------------------------------------------------------
# Auditor exception wrapping
# ---------------------------------------------------------------------------


def test_audit_wraps_auditor_exception(monkeypatch) -> None:
    class _BrokenAuditor:
        def audit(self, path, options):
            raise RuntimeError("parse failed")

    class _Cls:
        def __call__(self):
            return _BrokenAuditor()

    monkeypatch.setattr(graph_cmd, "_GraphAuditor", _Cls())
    monkeypatch.setattr(graph_cmd, "_AuditOptions", lambda **kw: SimpleNamespace(**kw))

    result = CliRunner().invoke(graph_cmd.app, ["audit", "graph.ttl"])

    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "parse failed" in str(result.exception)
