#!/usr/bin/env python3
# Copyright: 2026, Maximiliano Curia <maxy@debian.org>
#
# License: ISC
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#  .
#  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
#  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
#  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
#  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
#  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
#  PERFORMANCE OF THIS SOFTWARE.

"""Test harness for the codesearch CLI scripts under ``scripts/``.

Two facilities the codesearch-script tests share:

* ``load_script`` loads a script whose filename has hyphens (so it cannot be
  imported by module name) via ``importlib``, keeping that boilerplate in one
  place.
* ``FakeTransport`` / ``FakeResponse`` / ``make_http_error`` stand in for
  ``urllib.request.urlopen`` so a test can exercise the network/retry/cache
  paths offline - returning canned bytes or raising a chosen error, and
  recording how many times it was called.

This module is support code, not a test module (its name matches neither
pytest ``test_*``/``*_test`` pattern), so pytest does not collect it.
"""

from __future__ import annotations

import email.message
import importlib.util
import io
import sys
import urllib.error
from pathlib import Path
from types import ModuleType, TracebackType
from typing import Self

SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"


def load_script(filename: str) -> ModuleType:
    """Import a ``scripts/`` module whose filename has hyphens.

    The module is registered in ``sys.modules`` under a name with hyphens
    turned into underscores, then executed - which runs the script's own
    ``sys.path`` bootstrap so its ``decopy`` / sibling imports resolve.
    """
    path = SCRIPTS_DIR / filename
    mod_name = path.stem.replace("-", "_")
    spec = importlib.util.spec_from_file_location(mod_name, path)
    if spec is None or spec.loader is None:
        msg = f"cannot build an import spec for {path}"
        raise ImportError(msg)
    module = importlib.util.module_from_spec(spec)
    sys.modules[mod_name] = module
    spec.loader.exec_module(module)
    return module


class FakeResponse:
    """Context-manager stand-in for a ``urlopen`` response."""

    def __init__(self, data: bytes) -> None:
        self._data = data

    def __enter__(self) -> Self:
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        return

    def read(self, amt: int | None = None) -> bytes:
        return self._data if amt is None else self._data[:amt]


class FakeTransport:
    """Callable stand-in for ``urllib.request.urlopen``.

    Configured with a list of steps consumed in order; each step is either
    ``bytes`` (returned as a ``FakeResponse``) or an exception instance
    (raised). When ``repeat`` is set the last step is reused once the list is
    exhausted (handy for "always raises 503" retry tests); otherwise a further
    call is a test error. ``calls`` records how many times it ran.
    """

    def __init__(
        self, steps: list[bytes | BaseException], *, repeat: bool = False
    ) -> None:
        self._steps = list(steps)
        self._repeat = repeat
        self._last: bytes | BaseException | None = None
        self.calls = 0

    def __call__(self, req: object, timeout: float | None = None) -> FakeResponse:
        self.calls += 1
        if self._steps:
            self._last = self._steps.pop(0)
        elif not (self._repeat and self._last is not None):
            msg = f"FakeTransport called {self.calls} times, more than configured"
            raise AssertionError(msg)
        step = self._last
        if isinstance(step, BaseException):
            raise step
        assert step is not None
        return FakeResponse(step)


def make_http_error(
    code: int, *, retry_after: float | None = None, body: bytes = b""
) -> urllib.error.HTTPError:
    """Build an ``HTTPError`` with a chosen status, Retry-After and body."""
    hdrs = email.message.Message()
    if retry_after is not None:
        hdrs["Retry-After"] = str(retry_after)
    return urllib.error.HTTPError(
        "https://codesearch.debian.net/api/v1/search",
        code,
        "test-induced error",
        hdrs,
        io.BytesIO(body),
    )
