from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import re

from typer.testing import CliRunner

from worai import cli as root_cli
from worai.commands import graph as graph_cmd
from worai.core.graph import sync as graph_sync
from worai.errors import UsageError


@dataclass
class _FakeProfile:
    name: str
    api_key: str | None
    settings: dict


_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")


def _normalize_cli_output(text: str) -> str:
    return _ANSI_ESCAPE_RE.sub("", text)


def _patch_sdk_availability(monkeypatch) -> None:
    class _ConfigurationProvider:
        @staticmethod
        def create(_path=None):
            return None

    monkeypatch.setattr(graph_sync, "ConfigurationProvider", _ConfigurationProvider)
    monkeypatch.setattr(graph_sync, "KgBuildApplicationContainer", object())
    monkeypatch.setattr(graph_sync, "ProfileImportProtocol", object())


def test_graph_sync_help_from_root_cli() -> None:
    runner = CliRunner()
    result = runner.invoke(root_cli.app, ["graph", "sync", "--help"])
    output = _normalize_cli_output(result.output)

    assert result.exit_code == 0
    assert "--profile" in output
    assert "--debug" in output


def test_graph_sync_requires_profile_option() -> None:
    runner = CliRunner()
    result = runner.invoke(graph_cmd.app, [])
    output = _normalize_cli_output(result.output)

    assert result.exit_code == 2
    assert "--profile" in output


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

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

    monkeypatch.setattr(graph_cmd, "run_graph_sync", _stub_run)
    monkeypatch.setattr(graph_cmd, "resolve_config_path", lambda _path: Path("worai.toml"))
    runner = CliRunner()
    result = runner.invoke(graph_cmd.app, ["--profile", "zrh", "--debug"])

    assert result.exit_code == 0
    assert seen["options"].profile == "zrh"
    assert seen["options"].config_path == Path("worai.toml")
    assert seen["options"].debug is True


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

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

    monkeypatch.setattr(graph_cmd, "run_graph_sync", _stub_run)
    runner = CliRunner()
    cfg = tmp_path / "custom.toml"
    cfg.write_text("[profile.zrh]\napi_key='wl'\n", encoding="utf-8")
    result = runner.invoke(root_cli.app, ["--config", str(cfg), "graph", "sync", "--profile", "zrh"])

    assert result.exit_code == 0
    assert seen["options"].config_path == cfg


def test_load_service_account_json_supports_file_or_inline(tmp_path: Path) -> None:
    service_account = '{"type":"service_account"}'
    path = tmp_path / "service-account.json"
    path.write_text(service_account, encoding="utf-8")

    assert graph_sync.load_service_account_json(str(path)) == service_account
    assert graph_sync.load_service_account_json(service_account) == service_account


def test_build_dynamic_settings_supports_sitemap_source() -> None:
    lines, sheets_json = graph_sync._build_dynamic_settings(
        "wl_key",
        {
            "sitemap_url": "https://example.com/sitemap.xml",
            "sitemap_url_pattern": "example\\.com",
            "concurrency": 3,
        },
        google_search_console=False,
    )

    text = "\n".join(lines)
    assert "SITEMAP_URL" in text
    assert "SITEMAP_URL_PATTERN" in text
    assert sheets_json is None


def test_build_dynamic_settings_does_not_require_sheets_account_for_urls() -> None:
    lines, sheets_json = graph_sync._build_dynamic_settings(
        "wl_key",
        {
            "urls": ["https://example.com/a"],
            "sheets_service_account": "not-json-and-not-file",
        },
        google_search_console=False,
    )

    text = "\n".join(lines)
    assert "URLS" in text
    assert sheets_json is None


def test_build_dynamic_settings_requires_sheets_account_for_sheets_source() -> None:
    try:
        graph_sync._build_dynamic_settings(
            "wl_key",
            {
                "sheets_url": "https://docs.google.com/spreadsheets/d/abc",
                "sheets_name": "Sheet1",
            },
            google_search_console=False,
        )
        assert False, "Expected ValueError for missing sheets_service_account"
    except ValueError as exc:
        assert "sheets_service_account" in str(exc)


def test_resolve_google_search_console_default_false(tmp_path: Path) -> None:
    cfg = tmp_path / "worai.toml"
    cfg.write_text("[profile.acme]\napi_key='wl'\n", encoding="utf-8")

    assert graph_sync.resolve_google_search_console(cfg, "acme") is False


def test_resolve_google_search_console_global_and_profile_override(tmp_path: Path) -> None:
    cfg = tmp_path / "worai.toml"
    cfg.write_text(
        "google_search_console = false\n\n[profile.acme]\napi_key='wl'\ngoogle_search_console = true\n",
        encoding="utf-8",
    )

    assert graph_sync.resolve_google_search_console(cfg, "acme") is True


def test_run_graph_sync_async_keeps_cwd_root_and_passes_protocol(monkeypatch) -> None:
    profile = _FakeProfile(
        name="zrh",
        api_key="wl_key",
        settings={
            "overwrite": True,
            "concurrency": 3,
            "web_page_import_mode": "premium_scraper",
            "web_page_import_timeout": 1234,
            "urls": ["https://example.com/a"],
            "custom_toggle": "on",
        },
    )
    captured = {}

    class _ProfileMap(dict):
        def get(self, key):
            return profile if key == "zrh" else None

    class _FakeProtocol:
        def __init__(self, context, profile, root_dir, debug_dir):
            self.context = context
            self.profile = profile
            self.root_dir = root_dir
            self.debug_dir = debug_dir

    async def _stub_dynamic_runner(
        *,
        lines,
        sheets_service_account_json,
        debug,
        debug_profile_name,
        protocol_factory,
    ):
        captured["lines"] = lines
        captured["sheets_service_account_json"] = sheets_service_account_json
        protocol = await protocol_factory(context={"k": "v"}, debug_dir=Path("dbg"), workflow_config=None)
        captured["protocol"] = protocol
        captured["debug"] = debug
        captured["debug_profile_name"] = debug_profile_name

    _patch_sdk_availability(monkeypatch)
    monkeypatch.setattr(graph_sync, "load_profile_config", lambda _path: _ProfileMap())
    monkeypatch.setattr(graph_sync, "ProfileImportProtocol", _FakeProtocol)
    monkeypatch.setattr(graph_sync, "_run_dynamic_cloud_workflow", _stub_dynamic_runner)
    monkeypatch.setattr(graph_sync, "resolve_google_search_console", lambda *_args, **_kwargs: True)

    graph_sync.run_graph_sync(
        graph_sync.GraphSyncOptions(
            profile="zrh",
            config_path=Path("worai.toml"),
            debug=True,
        )
    )

    assert any(line.startswith("WORDLIFT_KEY") for line in captured["lines"])
    assert any(line.startswith("URLS") for line in captured["lines"])
    assert any(line.startswith("CUSTOM_TOGGLE") for line in captured["lines"])
    assert any(line == "GOOGLE_SEARCH_CONSOLE = True" for line in captured["lines"])
    assert captured["sheets_service_account_json"] is None
    assert captured["debug"] is True
    assert captured["debug_profile_name"] == "zrh"
    assert captured["protocol"].root_dir == Path.cwd()


def test_run_graph_sync_async_fails_when_profile_has_no_api_key(monkeypatch) -> None:
    profile = _FakeProfile(name="zrh", api_key=None, settings={"urls": ["https://example.com"]})

    class _ProfileMap(dict):
        def get(self, key):
            return profile if key == "zrh" else None

    _patch_sdk_availability(monkeypatch)
    monkeypatch.setattr(graph_sync, "load_profile_config", lambda _path: _ProfileMap())
    monkeypatch.setattr(graph_sync, "_run_dynamic_cloud_workflow", lambda **_kwargs: None)

    runner = CliRunner()
    result = runner.invoke(graph_cmd.app, ["--profile", "zrh"])

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