"""Collecting and running work the CMS has queued.

The direction is the point: the CMS never calls here. It writes a row, this
collects it on a poll it was already making, and reports back through the same
signed channel. Nothing new is exposed on either side.

The restart question is answered on the CMS side, not here — every command's
state lives there, so a process killed mid-flight loses nothing. What this side
owes is handlers that are safe to run twice, because an abandoned claim comes
back.
"""

from __future__ import annotations

import pytest

from app.api.client import AgentApiError
from app.whatsapp import handlers
from app.whatsapp.session import LinkRequest, SessionState, SessionStatus
from app.workflow.commands import Command, CommandProcessor, CommandResult


class FakeCms:
    """Stands in for the Agent API."""

    def __init__(self, queued=None, claim_error=None, report_error=None):
        self.queued = list(queued or [])
        self.claim_error = claim_error
        self.report_error = report_error
        self.reported: list[dict] = []

    def claim_command(self):
        if self.claim_error:
            raise self.claim_error

        return self.queued.pop(0) if self.queued else None

    def report_command(self, command_id, status, result=None, error="", retryable=True):
        if self.report_error:
            raise self.report_error

        self.reported.append({
            "id": command_id, "status": status, "result": result or {},
            "error": error, "retryable": retryable,
        })

        return {"ok": True}


def queued(command_type="whatsapp.status", **extra):
    return {"id": "cmd-1", "type": command_type, "payload": {}, "attempt": 1, **extra}


class StubProvider:
    """A linkable provider whose answers are scripted."""

    name = "evolution"

    def __init__(self, link=None, status=None, unlink=None):
        self._link = link
        self._status = status or SessionStatus(SessionState.DISCONNECTED, "nothing yet")
        self._unlink = unlink or SessionStatus(SessionState.DISCONNECTED, "logged out")
        self.calls: list[str] = []

    def start_link(self):
        self.calls.append("start_link")

        return self._link if self._link is not None else self._status

    def session_status(self):
        self.calls.append("session_status")

        return self._status

    def unlink(self):
        self.calls.append("unlink")

        return self._unlink


class NotLinkable:
    """Meta, in practice — no QR flow exists to offer."""

    name = "meta"

    def session_status(self):
        return SessionStatus(SessionState.CONNECTED, "business API")


# ── The processor ─────────────────────────────────────────────────────────


class TestClaimingAndReporting:
    def test_it_runs_a_queued_command(self):
        cms = FakeCms([queued()])
        processor = CommandProcessor(cms, {"whatsapp.status": lambda _: CommandResult.succeeded(state="ok")})

        assert processor.poll() == ["cmd-1"]
        assert cms.reported[0]["status"] == "completed"

    def test_an_empty_queue_does_nothing(self):
        cms = FakeCms([])

        assert CommandProcessor(cms).poll() == []
        assert cms.reported == []

    def test_it_stops_at_the_limit(self):
        """One busy installation must not monopolise the loop that also polls
        decisions and reports health."""
        cms = FakeCms([queued() for _ in range(10)])
        processor = CommandProcessor(cms, {"whatsapp.status": lambda _: CommandResult.succeeded()})

        assert len(processor.poll(limit=3)) == 3

    def test_an_unreachable_cms_is_not_a_command_failure(self):
        """There is nothing to report a failure *about* — the command was never
        collected. Reporting one would invent a result for work never done."""
        cms = FakeCms(claim_error=AgentApiError("connection refused"))

        assert CommandProcessor(cms).poll() == []
        assert cms.reported == []

    def test_a_handler_that_raises_is_reported_not_propagated(self):
        def explode(_payload):
            raise RuntimeError("the provider melted")

        cms = FakeCms([queued()])
        CommandProcessor(cms, {"whatsapp.status": explode}).poll()

        assert cms.reported[0]["status"] == "failed"
        assert "melted" in cms.reported[0]["error"]

    def test_an_unknown_command_type_is_permanent(self):
        """A CMS newer than this release. Retrying finds the same missing
        handler, and three identical failures say nothing the first did not."""
        cms = FakeCms([queued("whatsapp.teleport")])

        CommandProcessor(cms).poll()

        assert cms.reported[0]["status"] == "failed"
        assert cms.reported[0]["retryable"] is False

    def test_a_failed_report_does_not_stop_the_loop(self):
        """The work happened and the CMS did not hear. Its stale-claim sweep
        brings the command back, which is why handlers must be repeatable."""
        cms = FakeCms([queued()], report_error=AgentApiError("timeout"))

        assert CommandProcessor(cms, {"whatsapp.status": lambda _: CommandResult.succeeded()}).poll()

    def test_a_malformed_command_is_skipped(self):
        assert Command.from_response({"type": "whatsapp.status"}) is None
        assert Command.from_response({"id": "x"}) is None


# ── WhatsApp handlers ─────────────────────────────────────────────────────


class TestConnect:
    def test_it_returns_a_code_to_scan(self):
        provider = StubProvider(link=LinkRequest(payload="data:image/png;base64,AAAA"))

        result = handlers.connect(provider)

        assert result.ok
        assert result.data["qr"].endswith("AAAA")
        assert result.data["expires_at"]

    def test_an_already_connected_account_is_a_success(self):
        """The customer asked to be connected and they are. Reporting a failure
        would have them disconnect a working link to fix nothing."""
        provider = StubProvider(
            link=SessionStatus(SessionState.CONNECTED, "open", number="923001234567")
        )

        result = handlers.connect(provider)

        assert result.ok
        assert result.data["already_connected"] is True
        assert result.data["number"] == "923001234567"

    def test_a_provider_that_cannot_be_linked_fails_permanently(self):
        """No number of retries grows a QR flow onto an API that has none."""
        result = handlers.connect(NotLinkable())

        assert not result.ok
        assert result.retryable is False
        assert "cannot be linked" in result.error

    def test_an_unreachable_provider_is_worth_retrying(self):
        provider = StubProvider(link=SessionStatus(SessionState.UNAVAILABLE, "could not be reached"))

        result = handlers.connect(provider)

        assert not result.ok
        assert result.retryable is True


class TestStatus:
    def test_it_reports_the_connection(self):
        provider = StubProvider(
            status=SessionStatus(SessionState.CONNECTED, "open", number="923001234567")
        )

        result = handlers.status(provider)

        assert result.ok
        assert result.data["state"] == "connected"
        assert result.data["number"] == "923001234567"

    def test_unavailable_is_still_a_successful_answer(self):
        """The command was to find out. Finding out that the provider is down
        IS the answer — failing would retry three times to learn it again."""
        provider = StubProvider(status=SessionStatus(SessionState.UNAVAILABLE, "down"))

        result = handlers.status(provider)

        assert result.ok
        assert result.data["state"] == "unavailable"
        assert result.data["needs_attention"] is True

    def test_a_never_connected_deployment_needs_no_attention(self):
        provider = StubProvider(status=SessionStatus(SessionState.DISCONNECTED, "nothing yet"))

        assert handlers.status(provider).data["needs_attention"] is False


class TestDisconnect:
    def test_it_unlinks(self):
        provider = StubProvider()

        result = handlers.disconnect(provider)

        assert result.ok
        assert "unlink" in provider.calls

    def test_a_provider_with_no_link_fails_permanently(self):
        result = handlers.disconnect(NotLinkable())

        assert not result.ok
        assert result.retryable is False

    def test_an_unreachable_provider_is_worth_retrying(self):
        provider = StubProvider(unlink=SessionStatus(SessionState.UNAVAILABLE, "down"))

        result = handlers.disconnect(provider)

        assert not result.ok
        assert result.retryable is True


class TestRegistration:
    def test_all_three_are_wired(self):
        processor = CommandProcessor(FakeCms())
        handlers.register(processor, StubProvider())

        for kind in ("whatsapp.connect", "whatsapp.status", "whatsapp.disconnect"):
            assert kind in processor._handlers  # noqa: SLF001

    def test_the_provider_is_bound_not_chosen_per_command(self):
        """One deployment serves one installation and therefore one WhatsApp
        account (ADR-0001). A handler picking a provider from a payload would be
        the first step toward multi-tenancy."""
        provider = StubProvider()
        cms = FakeCms([queued("whatsapp.status", payload={"provider": "somebody-elses"})])
        processor = CommandProcessor(cms)
        handlers.register(processor, provider)

        processor.poll()

        assert provider.calls == ["session_status"]


class TestTheQrNeverLeaks:
    def test_it_is_absent_from_the_command_result_repr(self):
        result = CommandResult.succeeded(qr="SECRET-QR-PAYLOAD")

        # It has to travel to the CMS for the browser to draw it, so it IS in
        # `data`. What must not happen is it appearing anywhere incidentally.
        assert "SECRET-QR-PAYLOAD" in result.data["qr"]

    def test_the_link_request_it_came_from_still_masks_it(self):
        request = LinkRequest(payload="SECRET-QR-PAYLOAD")

        assert "SECRET-QR-PAYLOAD" not in repr(request)

    def test_a_failure_never_carries_one(self, caplog):
        provider = StubProvider(link=SessionStatus(SessionState.UNAVAILABLE, "down"))

        with caplog.at_level("DEBUG"):
            result = handlers.connect(provider)

        assert "qr" not in result.data
        assert "SECRET" not in caplog.text
