from __future__ import annotations

import asyncio
import hashlib
import importlib.util
import json
import os
import sys
from pathlib import Path
from types import SimpleNamespace
from typing import Any

import pytest
import yaml

HERE = Path(__file__).parent
SPEC = importlib.util.spec_from_file_location(
    "network_probe_v3", HERE / "network_probe_v3.py"
)
assert SPEC and SPEC.loader
m = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = m
SPEC.loader.exec_module(m)


def profile(
    tmp_path: Path,
    *,
    enabled: bool = True,
    token: str = "123456:secret",
    username: str = "bound_bot",
) -> Path:
    root = tmp_path / "non-default-profile"
    root.mkdir(mode=0o700)
    config = {
        "platforms": {
            "telegram": {
                "enabled": enabled,
                "extra": {
                    "adaptive_nutrition": {
                        "separate_bot": {"bot_username": username, "dedicated": True}
                    }
                },
            }
        }
    }
    (root / "config.yaml").write_text(yaml.safe_dump(config))
    (root / "config.yaml").chmod(0o600)
    (root / ".env").write_text(f"TELEGRAM_BOT_TOKEN={token}\n")
    (root / ".env").chmod(0o600)
    return root


class FakeBot:
    def __init__(
        self,
        *,
        username: str = "bound_bot",
        bot_id: int = 123456,
        error: Exception | None = None,
    ):
        self.username = username
        self.bot_id = bot_id
        self.error = error
        self.calls: list[str] = []

    async def __aenter__(self):
        self.calls.append("enter")
        return self

    async def __aexit__(self, *_args):
        self.calls.append("exit")

    async def get_me(self):
        self.calls.append("get_me")
        if self.error:
            raise self.error
        return SimpleNamespace(username=self.username, id=self.bot_id)


def test_loads_exact_non_default_profile_without_mutating_environment(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
    root = profile(tmp_path)
    monkeypatch.setenv("HERMES_HOME", "/wrong/default")
    before = dict(os.environ)
    binding = m.load_profile_binding(root)
    assert dict(os.environ) == before
    assert binding.profile == root.resolve()
    assert (
        binding.config_sha256
        == hashlib.sha256((root / "config.yaml").read_bytes()).hexdigest()
    )
    assert binding.username == "bound_bot"
    assert binding.bot_id == 123456
    assert binding.token == "123456:secret"
    assert binding.token not in repr(binding)


@pytest.mark.parametrize("enabled", [False, None])
def test_missing_or_disabled_telegram_fails_closed(
    tmp_path: Path, enabled: bool | None
):
    root = profile(tmp_path, enabled=True)
    document = yaml.safe_load((root / "config.yaml").read_text())
    if enabled is None:
        del document["platforms"]["telegram"]
    else:
        document["platforms"]["telegram"]["enabled"] = enabled
    (root / "config.yaml").write_text(yaml.safe_dump(document))
    with pytest.raises(
        m.ProbeFailure, match="enabled Telegram configuration unavailable"
    ):
        m.load_profile_binding(root)


def test_missing_token_fails_closed(tmp_path: Path):
    root = profile(tmp_path)
    (root / ".env").write_text("OTHER=value\n")
    with pytest.raises(m.ProbeFailure, match="token unavailable"):
        m.load_profile_binding(root)


def test_success_calls_only_get_me_and_emits_no_token(tmp_path: Path):
    binding = m.load_profile_binding(profile(tmp_path))
    bot = FakeBot()
    result = asyncio.run(m.getme_gate(binding, lambda _token: bot))
    assert bot.calls == ["enter", "get_me", "exit"]
    assert result["status"] == "READY_TELEGRAM_TRANSPORT"
    assert result["configured_username"] == "bound_bot"
    assert result["bot_id_match"] is True
    assert binding.token not in json.dumps(result)
    assert result["consumes_updates"] is False
    assert result["mutates_cursor"] is False
    assert result["writes_state"] is False


@pytest.mark.parametrize(
    ("username", "bot_id"), [("wrong_bot", 123456), ("bound_bot", 999999)]
)
def test_wrong_returned_bot_username_or_id_fails(
    tmp_path: Path, username: str, bot_id: int
):
    binding = m.load_profile_binding(profile(tmp_path))
    with pytest.raises(m.ProbeFailure, match="identity mismatch"):
        asyncio.run(
            m.getme_gate(
                binding, lambda _token: FakeBot(username=username, bot_id=bot_id)
            )
        )


def test_bad_gateway_fails_closed(tmp_path: Path):
    binding = m.load_profile_binding(profile(tmp_path))
    with pytest.raises(m.ProbeFailure, match="transport failure"):
        asyncio.run(
            m.getme_gate(
                binding, lambda _token: FakeBot(error=RuntimeError("Bad Gateway"))
            )
        )


def test_timeout_is_bounded_and_never_calls_other_method(tmp_path: Path):
    binding = m.load_profile_binding(profile(tmp_path))

    class PendingBot(FakeBot):
        async def get_me(self) -> Any:
            self.calls.append("get_me")
            await asyncio.Event().wait()

    bot = PendingBot()
    with pytest.raises(m.ProbeFailure, match="timeout"):
        asyncio.run(m.getme_gate(binding, lambda _token: bot, timeout=0.01))
    assert bot.calls == ["enter", "get_me", "exit"]
