from __future__ import annotations

import requests
from typer.testing import CliRunner

from worai.commands import graph as graph_cmd
from worai.core.graph import property_delete as property_delete_core
from worai.errors import UsageError


class _FakeResponse:
    def __init__(self, status_code: int = 200, text: str = "ok") -> None:
        self.status_code = status_code
        self.text = text


def test_normalize_predicate_accepts_curie() -> None:
    assert (
        property_delete_core.normalize_predicate("seovoc:html")
        == "https://w3id.org/seovoc/html"
    )


def test_normalize_predicate_rejects_unknown_prefix() -> None:
    try:
        property_delete_core.normalize_predicate("foo:bar")
        assert False, "Expected ValueError for unknown prefix."
    except ValueError as exc:
        assert "Unknown CURIE prefix" in str(exc)


def test_normalize_predicate_validation_errors() -> None:
    for raw in ("", "   ", "seovoc", "seovoc:"):
        try:
            property_delete_core.normalize_predicate(raw)
            assert False, f"Expected ValueError for {raw!r}"
        except ValueError:
            pass


def test_normalize_predicate_accepts_full_iri() -> None:
    iri = "https://example.com/predicate"
    assert property_delete_core.normalize_predicate(iri) == iri


def test_fetch_candidate_iris_errors_on_bad_shape(monkeypatch) -> None:
    monkeypatch.setattr(
        property_delete_core,
        "graphql_request",
        lambda *_args, **_kwargs: {"data": "bad"},
    )
    try:
        property_delete_core.fetch_candidate_iris(
            endpoint="https://api.wordlift.io/graphql",
            api_key="wl_123",
            predicate_iri="https://w3id.org/seovoc/html",
        )
        assert False, "Expected RuntimeError for malformed GraphQL response."
    except RuntimeError as exc:
        assert "data.entities" in str(exc)


def test_fetch_candidate_iris_filters_invalid_entries(monkeypatch) -> None:
    monkeypatch.setattr(
        property_delete_core,
        "graphql_request",
        lambda *_args, **_kwargs: {
            "data": {
                "entities": [
                    "bad",
                    {"iri": "  ", "value": "x"},
                    {"iri": "https://example.com/e1", "value": ""},
                    {"iri": "https://example.com/e2", "value": "v"},
                ]
            }
        },
    )
    out = property_delete_core.fetch_candidate_iris(
        endpoint="https://api.wordlift.io/graphql",
        api_key="wl_123",
        predicate_iri="https://w3id.org/seovoc/html",
    )
    assert out == ["https://example.com/e2"]


def test_remove_property_once_handles_request_exception(monkeypatch) -> None:
    def _raise(*_args, **_kwargs):
        raise requests.RequestException("network boom")

    monkeypatch.setattr(property_delete_core.requests, "patch", _raise)
    ok, message = property_delete_core._remove_property_once(
        entities_endpoint="https://api.wordlift.io/entities",
        api_key="wl_123",
        iri="https://example.com/e1",
        predicate_iri="https://w3id.org/seovoc/html",
        timeout=5,
    )
    assert ok is False
    assert "network boom" in message


def test_remove_property_once_handles_http_failure(monkeypatch) -> None:
    monkeypatch.setattr(
        property_delete_core.requests,
        "patch",
        lambda *_args, **_kwargs: _FakeResponse(status_code=500, text="server error"),
    )
    ok, message = property_delete_core._remove_property_once(
        entities_endpoint="https://api.wordlift.io/entities",
        api_key="wl_123",
        iri="https://example.com/e1",
        predicate_iri="https://w3id.org/seovoc/html",
        timeout=5,
    )
    assert ok is False
    assert "HTTP 500" in message


def test_remove_property_with_retries_sleeps_on_success_rate_delay(monkeypatch) -> None:
    sleeps: list[float] = []
    monkeypatch.setattr(property_delete_core, "time", type("T", (), {"sleep": sleeps.append})())
    monkeypatch.setattr(
        property_delete_core,
        "_remove_property_once",
        lambda **_kwargs: (True, ""),
    )
    iri, ok, message = property_delete_core._remove_property_with_retries(
        entities_endpoint="https://api.wordlift.io/entities",
        api_key="wl_123",
        iri="https://example.com/e1",
        predicate_iri="https://w3id.org/seovoc/html",
        timeout=5,
        retries=2,
        rate_delay=0.25,
    )
    assert iri == "https://example.com/e1"
    assert ok is True
    assert message == ""
    assert sleeps == [0.25]


def test_remove_property_with_retries_returns_failure_after_retries(monkeypatch) -> None:
    sleeps: list[float] = []
    monkeypatch.setattr(property_delete_core, "time", type("T", (), {"sleep": sleeps.append})())
    monkeypatch.setattr(
        property_delete_core,
        "_remove_property_once",
        lambda **_kwargs: (False, "HTTP 429"),
    )
    iri, ok, message = property_delete_core._remove_property_with_retries(
        entities_endpoint="https://api.wordlift.io/entities",
        api_key="wl_123",
        iri="https://example.com/e1",
        predicate_iri="https://w3id.org/seovoc/html",
        timeout=5,
        retries=3,
        rate_delay=0.0,
    )
    assert iri == "https://example.com/e1"
    assert ok is False
    assert message == "HTTP 429"
    assert sleeps == [1, 1]


def test_run_property_delete_dry_run(monkeypatch) -> None:
    called: dict[str, object] = {}

    def _stub_graphql_request(_endpoint, _api_key, _query, timeout=60, include_private=False):
        called["include_private"] = include_private
        return {
            "data": {
                "entities": [
                    {"iri": "https://example.com/e1", "value": "<html>...</html>"},
                    {"iri": "https://example.com/e2", "value": "x"},
                    {"iri": "https://example.com/e3", "value": None},
                ]
            }
        }

    monkeypatch.setattr(property_delete_core, "graphql_request", _stub_graphql_request)
    options = property_delete_core.PropertyDeleteOptions(
        api_key="wl_123",
        predicate="seovoc:html",
        dry_run=True,
    )
    summary = property_delete_core.run_property_delete(options)

    assert summary.predicate == "https://w3id.org/seovoc/html"
    assert summary.candidates == 2
    assert summary.attempted == 0
    assert summary.removed == 0
    assert summary.failed == 0
    assert called["include_private"] is True


def test_run_property_delete_applies_patch(monkeypatch) -> None:
    calls: list[tuple[str, str]] = []
    headers_seen: list[dict[str, str] | None] = []

    def _stub_graphql_request(_endpoint, _api_key, _query, timeout=60, include_private=False):
        return {
            "data": {
                "entities": [
                    {"iri": "https://example.com/e1", "value": "x"},
                    {"iri": "https://example.com/e2", "value": "y"},
                ]
            }
        }

    def _stub_patch(url, headers=None, data=None, timeout=60):
        calls.append((url, data or ""))
        headers_seen.append(headers)
        return _FakeResponse(status_code=200, text="ok")

    monkeypatch.setattr(property_delete_core, "graphql_request", _stub_graphql_request)
    monkeypatch.setattr(property_delete_core.requests, "patch", _stub_patch)

    options = property_delete_core.PropertyDeleteOptions(
        api_key="wl_123",
        predicate="seovoc:html",
        dry_run=False,
        workers=1,
    )
    summary = property_delete_core.run_property_delete(options)

    assert summary.candidates == 2
    assert summary.attempted == 2
    assert summary.removed == 2
    assert summary.failed == 0
    assert len(calls) == 2
    assert '"path": "/https://w3id.org/seovoc/html"' in calls[0][1]
    assert headers_seen[0] is not None
    assert headers_seen[0]["X-include-Private"] == "true"


def test_run_property_delete_handles_limit_and_empty_candidates(monkeypatch) -> None:
    monkeypatch.setattr(
        property_delete_core,
        "graphql_request",
        lambda *_args, **_kwargs: {"data": {"entities": []}},
    )
    summary = property_delete_core.run_property_delete(
        property_delete_core.PropertyDeleteOptions(
            api_key="wl_123",
            predicate="seovoc:html",
            dry_run=False,
            limit=1,
        )
    )
    assert summary.candidates == 0
    assert summary.attempted == 0
    assert summary.removed == 0
    assert summary.failed == 0


def test_run_property_delete_counts_failures(monkeypatch) -> None:
    monkeypatch.setattr(
        property_delete_core,
        "graphql_request",
        lambda *_args, **_kwargs: {
            "data": {
                "entities": [
                    {"iri": "https://example.com/e1", "value": "x"},
                    {"iri": "https://example.com/e2", "value": "y"},
                ]
            }
        },
    )
    monkeypatch.setattr(
        property_delete_core,
        "_remove_property_with_retries",
        lambda **kwargs: (
            kwargs["iri"],
            kwargs["iri"].endswith("/e1"),
            "" if kwargs["iri"].endswith("/e1") else "fail",
        ),
    )
    summary = property_delete_core.run_property_delete(
        property_delete_core.PropertyDeleteOptions(
            api_key="wl_123",
            predicate="seovoc:html",
            dry_run=False,
            workers=1,
        )
    )
    assert summary.attempted == 2
    assert summary.removed == 1
    assert summary.failed == 1


def test_graph_property_delete_cli_calls_core(monkeypatch) -> None:
    seen: list[property_delete_core.PropertyDeleteOptions] = []

    def _stub_run(options):
        seen.append(options)
        if options.dry_run:
            return property_delete_core.PropertyDeleteSummary(
                predicate="https://w3id.org/seovoc/html",
                candidates=2,
                attempted=0,
                removed=0,
                failed=0,
            )
        return property_delete_core.PropertyDeleteSummary(
            predicate="https://w3id.org/seovoc/html",
            candidates=2,
            attempted=2,
            removed=2,
            failed=0,
        )

    monkeypatch.setattr(graph_cmd, "run_property_delete", _stub_run)
    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("wl_123", explicit_profile or "default", "worai.toml"))

    runner = CliRunner()
    result = runner.invoke(
        graph_cmd.app,
        ["property", "delete", "seovoc:html", "--yes"],
    )

    assert result.exit_code == 0
    assert len(seen) == 2
    assert seen[0].dry_run is True
    assert seen[1].dry_run is False


def test_graph_property_delete_cli_dry_run_stops_after_first_call(monkeypatch) -> None:
    seen: list[property_delete_core.PropertyDeleteOptions] = []

    def _stub_run(options):
        seen.append(options)
        return property_delete_core.PropertyDeleteSummary(
            predicate="https://w3id.org/seovoc/html",
            candidates=3,
            attempted=0,
            removed=0,
            failed=0,
        )

    monkeypatch.setattr(graph_cmd, "run_property_delete", _stub_run)
    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("wl_123", explicit_profile or "default", "worai.toml"))
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html", "--dry-run"])
    assert result.exit_code == 0
    assert len(seen) == 1
    assert seen[0].dry_run is True


def test_graph_property_delete_cli_returns_when_no_candidates(monkeypatch) -> None:
    seen: list[property_delete_core.PropertyDeleteOptions] = []

    def _stub_run(options):
        seen.append(options)
        return property_delete_core.PropertyDeleteSummary(
            predicate="https://w3id.org/seovoc/html",
            candidates=0,
            attempted=0,
            removed=0,
            failed=0,
        )

    monkeypatch.setattr(graph_cmd, "run_property_delete", _stub_run)
    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("wl_123", explicit_profile or "default", "worai.toml"))
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html", "--yes"])
    assert result.exit_code == 0
    assert len(seen) == 1


def test_graph_property_delete_cli_surfaces_core_value_errors(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("wl_123", explicit_profile or "default", "worai.toml"))
    monkeypatch.setattr(graph_cmd, "run_property_delete", lambda _opts: (_ for _ in ()).throw(ValueError("bad")))
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html", "--yes"])
    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "bad" in str(result.exception)


def test_graph_property_delete_cli_prompts_confirmation_without_yes(monkeypatch) -> None:
    seen_confirm = {"called": False}
    seen_runs: list[property_delete_core.PropertyDeleteOptions] = []

    def _stub_run(options):
        seen_runs.append(options)
        if options.dry_run:
            return property_delete_core.PropertyDeleteSummary(
                predicate="https://w3id.org/seovoc/html",
                candidates=1,
                attempted=0,
                removed=0,
                failed=0,
            )
        return property_delete_core.PropertyDeleteSummary(
            predicate="https://w3id.org/seovoc/html",
            candidates=1,
            attempted=1,
            removed=1,
            failed=0,
        )

    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("wl_123", explicit_profile or "default", "worai.toml"))
    monkeypatch.setattr(graph_cmd, "run_property_delete", _stub_run)

    def _stub_confirm(*_args, **_kwargs):
        seen_confirm["called"] = True
        return True

    monkeypatch.setattr(graph_cmd.typer, "confirm", _stub_confirm)
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html"])
    assert result.exit_code == 0
    assert seen_confirm["called"] is True
    assert len(seen_runs) == 2


def test_graph_property_delete_cli_raises_when_failures_present(monkeypatch) -> None:
    calls = {"count": 0}

    def _stub_run(options):
        calls["count"] += 1
        if options.dry_run:
            return property_delete_core.PropertyDeleteSummary(
                predicate="https://w3id.org/seovoc/html",
                candidates=1,
                attempted=0,
                removed=0,
                failed=0,
            )
        return property_delete_core.PropertyDeleteSummary(
            predicate="https://w3id.org/seovoc/html",
            candidates=1,
            attempted=1,
            removed=0,
            failed=1,
        )

    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: ("wl_123", explicit_profile or "default", "worai.toml"))
    monkeypatch.setattr(graph_cmd, "run_property_delete", _stub_run)
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html", "--yes"])
    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "Some entities failed to patch" in str(result.exception)
    assert calls["count"] == 2


def test_graph_sync_create_prompts_for_destination(monkeypatch, tmp_path) -> None:
    captured: dict[str, object] = {}

    def _stub_prompt(_text: str) -> str:
        return str(tmp_path / "from-prompt")

    def _stub_create(options):
        captured["destination"] = options.destination

    monkeypatch.setattr(graph_cmd.typer, "prompt", _stub_prompt)
    monkeypatch.setattr(graph_cmd, "run_graph_sync_create", _stub_create)
    graph_cmd.create(
        destination=None,
        template="gh:wordlift/graph-sync-template",
        defaults=False,
        data_file=None,
        vcs_ref=None,
        non_interactive=False,
        force=False,
    )
    assert str(captured["destination"]).endswith("from-prompt")


def test_graph_property_delete_cli_requires_api_key(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx, explicit_profile=None: (None, explicit_profile or "default", "worai.toml"))
    runner = CliRunner()
    result = runner.invoke(graph_cmd.app, ["property", "delete", "seovoc:html", "--yes"])

    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "WORDLIFT_API_KEY is required" in str(result.exception)
