"""Tests for the release provider abstraction and Gitea probe."""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock

import pytest

from notify_bridge_core.release import build_release_provider, is_valid_repo
from notify_bridge_core.release.base import (
    ReleaseErrorCode,
    ReleaseProviderKind,
    compare_versions,
    is_newer,
    normalise_version,
)
from notify_bridge_core.release.gitea import GiteaReleaseProvider


# --- pure utilities ---------------------------------------------------------


def test_normalise_version_strips_v_prefix() -> None:
    assert normalise_version("v1.2.3") == "1.2.3"
    assert normalise_version("V1.2.3") == "1.2.3"
    assert normalise_version("1.2.3") == "1.2.3"
    assert normalise_version("") == ""
    # Only strip ``v`` when followed by a digit — guard against names like
    # ``vendor-1`` being mangled into ``endor-1``.
    assert normalise_version("vendor-1") == "vendor-1"


@pytest.mark.parametrize(
    ("a", "b", "expected"),
    [
        ("0.7.3", "0.7.2", 1),
        ("0.7.2", "0.7.3", -1),
        ("0.7.2", "0.7.2", 0),
        ("v0.7.3", "0.7.2", 1),
        ("1.0.0", "0.9.99", 1),
        # Stable beats prerelease at equal numerics (tie-break).
        ("0.7.2-rc1", "0.7.2", -1),
        ("0.7.2", "0.7.2-rc1", 1),
        # Implicit prerelease form ``1.0a2`` must NOT extract ``2`` as a
        # third numeric segment — equal to ``1.0`` stable, then stable wins.
        ("1.0a2", "1.0", -1),
        ("", "0.0.0", 0),
    ],
)
def test_compare_versions(a: str, b: str, expected: int) -> None:
    assert compare_versions(a, b) == expected


def test_is_newer_is_strict() -> None:
    assert is_newer("0.7.3", "0.7.2") is True
    assert is_newer("0.7.2", "0.7.2") is False
    # A pre-release of the next minor should still be flagged as newer when
    # explicitly fetched with include_prereleases=True at the provider level.
    assert is_newer("0.7.3-rc1", "0.7.2") is True


def test_is_valid_repo() -> None:
    assert is_valid_repo("alexei.dolgolyov/notify-bridge") is True
    assert is_valid_repo("a/b") is True
    assert is_valid_repo("a_b/c.d-e") is True
    assert is_valid_repo("") is False
    assert is_valid_repo("no-slash") is False
    # Path-traversal attempts.
    assert is_valid_repo("foo/bar/../admin") is False
    assert is_valid_repo("foo/bar/baz") is False
    assert is_valid_repo("foo/../bar") is False
    # Embedded special chars.
    assert is_valid_repo("foo@bar/baz") is False
    assert is_valid_repo("foo/bar?x=1") is False


# --- registry ---------------------------------------------------------------


def test_registry_returns_none_for_disabled() -> None:
    assert build_release_provider("disabled", session=MagicMock(), url="x", repo="a/b") is None


def test_registry_returns_none_for_unknown_kind() -> None:
    assert build_release_provider("svn", session=MagicMock(), url="x", repo="a/b") is None


def test_registry_gitea_requires_url_and_valid_repo() -> None:
    sess = MagicMock()
    assert build_release_provider("gitea", session=sess, url="", repo="a/b") is None
    assert build_release_provider("gitea", session=sess, url="https://x", repo="") is None
    # Path traversal blocked by repo validation.
    assert build_release_provider("gitea", session=sess, url="https://x", repo="a/b/../c") is None
    provider = build_release_provider("gitea", session=sess, url="https://x", repo="a/b")
    assert isinstance(provider, GiteaReleaseProvider)
    assert provider.kind is ReleaseProviderKind.GITEA


# --- Gitea provider ---------------------------------------------------------


def _gitea_payload(**overrides: Any) -> list[dict[str, Any]]:
    base = {
        "tag_name": "v0.7.3",
        "name": "v0.7.3",
        "html_url": "https://git.example.com/owner/repo/releases/tag/v0.7.3",
        "body": "Notes",
        "published_at": "2026-05-01T00:00:00Z",
        "draft": False,
        "prerelease": False,
    }
    base.update(overrides)
    return [base]


class _FakeContent:
    def __init__(self, raw: bytes) -> None:
        self._raw = raw

    async def read(self, n: int = -1) -> bytes:
        return self._raw if n < 0 else self._raw[:n]


class _FakeResponse:
    def __init__(self, status: int, payload: Any) -> None:
        self.status = status
        import json

        self.content = _FakeContent(json.dumps(payload).encode("utf-8"))
        self._payload = payload

    async def json(self) -> Any:
        return self._payload

    async def __aenter__(self) -> "_FakeResponse":
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:
        return None


def _session_with(payload: Any, status: int = 200) -> MagicMock:
    """Return a session whose `.get()` yields a fresh response per call.

    Using ``side_effect`` rather than ``return_value`` ensures multiple
    awaited fetches don't share mutable response state across tests.
    """
    sess = MagicMock()
    sess.get = MagicMock(side_effect=lambda *a, **kw: _FakeResponse(status, payload))
    return sess


@pytest.fixture(autouse=True)
def _allow_private_urls(monkeypatch: pytest.MonkeyPatch) -> None:
    """SSRF guard rejects example.com → publicly resolvable, so tests pass.

    But we explicitly enable the bypass to remove DNS-resolution flakiness
    from CI runs.
    """
    monkeypatch.setenv("NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS", "1")
    # Reload the ssrf module to pick up the env var (it's read at import).
    import importlib

    import notify_bridge_core.notifications.ssrf as ssrf_mod
    importlib.reload(ssrf_mod)


async def test_gitea_fetch_latest_happy_path() -> None:
    sess = _session_with(_gitea_payload())
    provider = GiteaReleaseProvider(sess, "https://git.example.com/", "owner/repo")

    info = await provider.fetch_latest(include_prereleases=False)
    assert info is not None
    assert info.tag == "v0.7.3"
    assert info.version == "0.7.3"
    assert info.url == "https://git.example.com/owner/repo/releases/tag/v0.7.3"
    assert info.prerelease is False


async def test_gitea_skips_prereleases_by_default() -> None:
    payload = _gitea_payload(prerelease=True)
    sess = _session_with(payload)
    provider = GiteaReleaseProvider(sess, "https://x.example.com", "a/b")
    assert await provider.fetch_latest(include_prereleases=False) is None


async def test_gitea_includes_prereleases_when_asked() -> None:
    payload = _gitea_payload(prerelease=True)
    sess = _session_with(payload)
    provider = GiteaReleaseProvider(sess, "https://x.example.com", "a/b")
    info = await provider.fetch_latest(include_prereleases=True)
    assert info is not None
    assert info.prerelease is True


async def test_gitea_skips_drafts() -> None:
    payload = _gitea_payload(draft=True)
    sess = _session_with(payload)
    provider = GiteaReleaseProvider(sess, "https://x.example.com", "a/b")
    assert await provider.fetch_latest(include_prereleases=True) is None


async def test_gitea_returns_none_on_http_error() -> None:
    sess = _session_with([], status=500)
    provider = GiteaReleaseProvider(sess, "https://x.example.com", "a/b")
    assert await provider.fetch_latest() is None


async def test_gitea_test_returns_structured_status() -> None:
    sess = _session_with(_gitea_payload())
    provider = GiteaReleaseProvider(sess, "https://x.example.com", "a/b")
    result = await provider.test()
    assert result["ok"] is True
    assert result["info"] is not None
    assert result["error"] is None


async def test_gitea_test_reports_http_error() -> None:
    sess = _session_with([], status=404)
    provider = GiteaReleaseProvider(sess, "https://x.example.com", "a/b")
    result = await provider.test()
    assert result["ok"] is False
    assert result["info"] is None
    # Taxonomy code, not a raw exception string.
    assert result["error"] in {code.value for code in ReleaseErrorCode}


def test_gitea_constructor_validates_repo_format() -> None:
    with pytest.raises(ValueError):
        GiteaReleaseProvider(MagicMock(), "https://x.example.com", "no-slash")
    with pytest.raises(ValueError):
        GiteaReleaseProvider(MagicMock(), "https://x.example.com", "foo/bar/../baz")
    with pytest.raises(ValueError):
        GiteaReleaseProvider(MagicMock(), "", "owner/repo")
