from __future__ import annotations

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


def test_graph_sync_run_uses_ctx_config_path(monkeypatch, tmp_path: Path) -> None:
    seen = {}

    def _stub_run(options):
        seen["options"] = options

    monkeypatch.setattr(graph_cmd, "run_graph_sync", _stub_run)

    result = CliRunner().invoke(
        graph_cmd.app,
        ["sync", "run", "--debug"],
        obj={"config_path": tmp_path / "custom.toml", "profile": "acme"},
    )

    assert result.exit_code == 0
    assert seen["options"].profile == "acme"
    assert seen["options"].config_path == tmp_path / "custom.toml"
    assert seen["options"].debug is True


def test_graph_sync_run_resolves_default_config_and_wraps_errors(monkeypatch, tmp_path: Path) -> None:
    monkeypatch.setattr(graph_cmd, "resolve_config_path", lambda _x: tmp_path / "auto.toml")
    monkeypatch.setattr(graph_cmd, "run_graph_sync", lambda _o: (_ for _ in ()).throw(ValueError("bad profile")))

    result = CliRunner().invoke(graph_cmd.app, ["sync", "run"], obj={"profile": "acme"})

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


def test_graph_sync_create_requires_destination_in_non_interactive_mode() -> None:
    result = CliRunner().invoke(graph_cmd.app, ["sync", "create", "--non-interactive"])

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


def test_graph_sync_create_prompts_destination(monkeypatch) -> None:
    seen = {}
    monkeypatch.setattr(graph_cmd.typer, "prompt", lambda _msg: "./dest")
    monkeypatch.setattr(graph_cmd, "run_graph_sync_create", lambda options: seen.setdefault("options", options))

    result = CliRunner().invoke(graph_cmd.app, ["sync", "create", "--defaults"])

    assert result.exit_code == 0
    assert seen["options"].destination == Path("./dest")


def test_graph_validate_input_normalizers_and_issue_helpers() -> None:
    assert graph_cmd._normalize_level("Warning") == "warning"
    assert graph_cmd._normalize_output_format("JSON") == "json"
    with pytest.raises(UsageError):
        graph_cmd._normalize_level("bad")
    with pytest.raises(UsageError):
        graph_cmd._normalize_output_format("bad")

    issue_dict = {"level": "warning", "message": "m"}
    assert graph_cmd._issue_to_dict(issue_dict) == issue_dict
    issue_obj = SimpleNamespace(level="error", severity="Violation", focus_node=None, result_path=None, rule_id="r1", rule_set="g", message="x")
    converted = graph_cmd._issue_to_dict(issue_obj)
    assert converted["level"] == "error"
    assert converted["rule_id"] == "r1"


def test_graph_validate_resolve_shape_specs_paths(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "_sdk_resolve_shape_specs", None)
    assert graph_cmd._resolve_shape_specs(None, None, ["extra.ttl"]) == ["extra.ttl"]
    with pytest.raises(UsageError, match="wordlift-sdk>=6.1.0"):
        graph_cmd._resolve_shape_specs(["a"], None, None)

    monkeypatch.setattr(graph_cmd, "_sdk_resolve_shape_specs", lambda **kwargs: ["resolved", kwargs["extra_shapes"][0]])
    resolved = graph_cmd._resolve_shape_specs(["a"], ["b"], ["extra.ttl"])
    assert resolved == ["resolved", "extra.ttl"]


def test_graph_validate_filtered_issues_paths(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "_sdk_extract_validation_issues", None)
    monkeypatch.setattr(graph_cmd, "_sdk_filter_validation_issues", None)

    ok = SimpleNamespace(conforms=True, warning_count=0)
    assert graph_cmd._filtered_issues(ok, "warning") == []

    warn = SimpleNamespace(conforms=True, warning_count=1)
    warning_issues = graph_cmd._filtered_issues(warn, "warning")
    assert warning_issues and warning_issues[0]["level"] == "warning"

    err = SimpleNamespace(conforms=False, warning_count=0)
    error_issues = graph_cmd._filtered_issues(err, "error")
    assert error_issues and error_issues[0]["level"] == "error"

    monkeypatch.setattr(graph_cmd, "_sdk_extract_validation_issues", lambda _r: ["a"])
    monkeypatch.setattr(graph_cmd, "_sdk_filter_validation_issues", lambda issues, level: [] if level == "error" else issues)
    assert graph_cmd._filtered_issues(ok, "error") == []
    assert graph_cmd._filtered_issues(ok, "warning") == ["a"]


def test_graph_validate_json_single_input_and_error_wrapping(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "_sdk_resolve_shape_specs", lambda **_k: [])
    monkeypatch.setattr(graph_cmd, "_sdk_validate_file", lambda *_a, **_k: SimpleNamespace(conforms=True, warning_count=0))
    monkeypatch.setattr(graph_cmd, "_sdk_extract_validation_issues", lambda _r: [])
    monkeypatch.setattr(graph_cmd, "_sdk_filter_validation_issues", lambda issues, level: issues)

    result = CliRunner().invoke(graph_cmd.app, ["validate", "one.ttl", "--format", "json"])
    assert result.exit_code == 0
    assert '"input": "one.ttl"' in result.output
    assert result.output.strip().startswith("{")

    monkeypatch.setattr(graph_cmd, "_sdk_resolve_shape_specs", lambda **_k: (_ for _ in ()).throw(RuntimeError("bad shape")))
    result = CliRunner().invoke(graph_cmd.app, ["validate", "one.ttl"])
    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "bad shape" in str(result.exception)

    monkeypatch.setattr(graph_cmd, "_sdk_resolve_shape_specs", lambda **_k: [])
    monkeypatch.setattr(graph_cmd, "_sdk_validate_file", lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError("parse error")))
    result = CliRunner().invoke(graph_cmd.app, ["validate", "one.ttl"])
    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "Failed to validate one.ttl" in str(result.exception)


def test_graph_property_delete_paths(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx: (None, "default", Path("worai.toml")))
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html"])
    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "WORDLIFT_API_KEY is required" in str(result.exception)

    monkeypatch.setattr(
        graph_cmd,
        "resolve_profile_api_key",
        lambda _ctx: (_ for _ in ()).throw(ValueError("missing profile")),
    )
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html"])
    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "missing profile" in str(result.exception)

    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx: ("wl_key", "default", Path("worai.toml")))
    monkeypatch.setattr(
        graph_cmd,
        "run_property_delete",
        lambda _o: (_ for _ in ()).throw(ValueError("bad predicate")),
    )
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html"])
    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "bad predicate" in str(result.exception)


def test_graph_property_delete_dry_run_and_execute(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx: ("wl_key", "default", Path("worai.toml")))

    calls = []

    def _run(options):
        calls.append(options)
        if options.dry_run:
            return SimpleNamespace(predicate="seovoc:html", candidates=2, attempted=0, removed=0, failed=0)
        return SimpleNamespace(predicate="seovoc:html", candidates=2, attempted=2, removed=2, failed=0)

    monkeypatch.setattr(graph_cmd, "run_property_delete", _run)
    monkeypatch.setattr(graph_cmd.typer, "confirm", lambda *_a, **_k: True)

    dry_run = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html", "--dry-run"])
    assert dry_run.exit_code == 0
    assert len(calls) == 1
    assert calls[0].dry_run is True

    full_run = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html"])
    assert full_run.exit_code == 0
    assert len(calls) == 3
    assert calls[1].dry_run is True
    assert calls[2].dry_run is False


def test_graph_property_delete_zero_and_failed_paths(monkeypatch) -> None:
    monkeypatch.setattr(graph_cmd, "resolve_profile_api_key", lambda _ctx: ("wl_key", "default", Path("worai.toml")))

    monkeypatch.setattr(
        graph_cmd,
        "run_property_delete",
        lambda _o: SimpleNamespace(predicate="seovoc:html", candidates=0, attempted=0, removed=0, failed=0),
    )
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html"])
    assert result.exit_code == 0

    calls = []

    def _run_failed(options):
        calls.append(options.dry_run)
        if options.dry_run:
            return SimpleNamespace(predicate="seovoc:html", candidates=1, attempted=0, removed=0, failed=0)
        return SimpleNamespace(predicate="seovoc:html", candidates=1, attempted=1, removed=0, failed=1)

    monkeypatch.setattr(graph_cmd, "run_property_delete", _run_failed)
    result = CliRunner().invoke(graph_cmd.app, ["property", "delete", "seovoc:html", "--yes"])
    assert result.exit_code == 1
    assert isinstance(result.exception, UsageError)
    assert "failed to patch" in str(result.exception).lower()
    assert calls == [True, False]
