"""
Dobles de Google, alimentados con respuestas grabadas.

El corte está en el cliente HTTP y no en nuestro código: se simula lo que Google
contesta, no lo que nosotros concluimos. Un doble que devolviera «la credencial
es válida» probaría que sabemos escribir un `if`; uno que devuelve el JSON que
Google devolvió de verdad prueba que sabemos leerlo.

Las respuestas de `tests/fixtures/gsc/` son copias de las reales, con los datos
identificatorios cambiados. Su forma es exactamente la que llega.
"""

import json
from pathlib import Path

from django.conf import settings
from googleapiclient.errors import HttpError

FIXTURES = Path(settings.BASE_DIR) / 'tests' / 'fixtures' / 'gsc'


def response(name: str) -> dict:
    return json.loads((FIXTURES / f'{name}.json').read_text(encoding='utf-8'))


class _Response:
    """Imita el objeto de respuesta que espera HttpError."""

    def __init__(self, status: int):
        self.status = status
        self.reason = ''


def http_error(name: str) -> HttpError:
    """Arma el error tal como lo levanta la biblioteca de Google."""
    body = response(name)
    return HttpError(_Response(body['error']['code']), json.dumps(body).encode())


class FakeClient:
    """
    Reemplaza a SearchConsoleClient sin tocar la red.

    Registra las reservas con las que se lo llamó: varios tests comprueban no
    sólo el resultado sino que la llamada se haya hecho con cupo concedido, que
    es el invariante que el producto no puede romper.
    """

    def __init__(self, **results):
        self.results = results
        self.calls: list[tuple[str, object]] = []

    def _resolve(self, operation, reservation):
        self.calls.append((operation, reservation))

        if reservation is None or reservation.granted <= 0:
            raise AssertionError(f'«{operation}» se llamó sin reserva concedida.')

        result = self.results.get(operation)

        if isinstance(result, HttpError):
            # El doble traduce igual que el cliente real. Si levantara el error
            # crudo de Google, quien llama recibiría algo que en producción
            # nunca ve, y de paso la clasificación —que es la lógica que hay que
            # probar— quedaría sin ejercitar.
            from apps.gsc.client import _from_http_error

            raise _from_http_error(result)

        if isinstance(result, Exception):
            raise result
        if callable(result):
            return result()
        return result if result is not None else {}

    def list_sites(self, *, reservation):
        return self._resolve('list_sites', reservation)

    def get_site(self, *, property_uri, reservation):
        return self._resolve('get_site', reservation)

    def list_sitemaps(self, *, property_uri, reservation):
        return self._resolve('list_sitemaps', reservation)

    def submit_sitemap(self, *, property_uri, location, reservation):
        return self._resolve('submit_sitemap', reservation)

    def delete_sitemap(self, *, property_uri, location, reservation):
        return self._resolve('delete_sitemap', reservation)

    def inspect_url(self, *, property_uri, url, reservation):
        return self._resolve('inspect_url', reservation)

    def search_analytics(
        self,
        *,
        property_uri,
        start_date,
        end_date,
        dimensions,
        row_limit=25_000,
        start_row=0,
    ):
        """
        La única operación del cliente que **no** pide reserva.

        Es a propósito y está declarada también en `test_gsc_guard`: el
        presupuesto de cuota modela el cupo agotable de la inspección de URLs, y
        Search Analytics tiene un límite de tasa que se renueva solo. Un doble que
        exigiera reserva acá impondría una regla que la aplicación decidió no
        tener, y haría fallar al módulo por cumplirla bien.

        La respuesta se declara por combinación de dimensiones, no una sola para
        todas: el módulo pregunta dos veces por rango —una con la dimensión de
        página y otra sin ella— y lo que hay que poder probar es justamente que
        las dos respuestas son distintas y no se suman.
        """
        key = f'search_analytics:{",".join(dimensions)}'
        self.calls.append((key, None))

        result = self.results.get(key, self.results.get('search_analytics'))

        if isinstance(result, Exception):
            raise result
        if callable(result):
            return result(start_date, end_date, start_row)
        return result if result is not None else {}


class FakeIndexingClient:
    """
    Reemplaza a `IndexingClient` sin tocar la red.

    Se le declara la respuesta **por dirección**, y no una sola para todas,
    porque lo que hay que poder probar es la mezcla: un lote donde la tercera
    falla y las dos primeras ya salieron es el caso que define el producto.

    A diferencia de `FakeClient`, este doble no exige reserva de cupo: la
    indexación no lleva contador propio a propósito, y un doble que lo pidiera
    estaría imponiendo una regla que la aplicación decidió no tener.
    """

    def __init__(self, answers=None, default=None):
        # Cada valor es un `IndexingCall` ya armado, o el error clasificado que
        # hay que devolver como tal. La clave es la dirección.
        self.answers = answers or {}
        self.default = default
        self.published: list[str] = []

    def publish(self, url, *, notification_type='URL_UPDATED'):
        from apps.indexing.client import IndexingCall
        from apps.indexing.errors import message_for

        self.published.append(url)

        answer = self.answers.get(url, self.default)
        request_body = {'url': url, 'type': notification_type}

        if answer is None:
            # Lo que Google contesta cuando acepta: el eco del aviso con su
            # momento. Se copia la forma real porque es lo que se guarda como
            # evidencia, y una forma inventada probaría que sabemos guardar un
            # diccionario, no que guardamos lo que llegó.
            return IndexingCall(
                request_body=request_body,
                response_status=200,
                response_body={
                    'urlNotificationMetadata': {
                        'url': url,
                        'latestUpdate': {'url': url, 'type': notification_type},
                    }
                },
            )

        if isinstance(answer, IndexingCall):
            return answer

        code, status = answer
        return IndexingCall(
            request_body=request_body,
            response_status=status,
            response_body={'error': {'code': status, 'status': code}},
            error_code=code,
            error_message=message_for(code),
        )
