"""Linking a customer's WhatsApp account.

The security case here is not obvious until it is said out loud: **a QR code is
a credential**. Scanning one links a device to a WhatsApp account and grants
read access to every conversation in it. So it is treated the way the Phase 9
audit taught — masked in reprs, short-lived, and never anywhere a log file can
reach.

The other thing under test is honesty about what a provider can do. Meta has no
QR flow and never will; a customer must be told that before a Connect button is
drawn, not after they press one.
"""

from __future__ import annotations

import json
from datetime import UTC, datetime, timedelta

import pytest

from app.support.http import HttpResponse
from app.whatsapp.evolution import EvolutionProvider
from app.whatsapp.meta import MetaBusinessProvider, MetaConfig
from app.whatsapp.session import (
    QR_LIFETIME_SECONDS,
    LinkRequest,
    SessionState,
    SessionStatus,
    status_of,
    supports_linking,
)


class FakeEvolution:
    """Answers Evolution's session endpoints from a script."""

    def __init__(self, routes: dict[str, tuple[int, object]] | None = None, fail: bool = False):
        self.routes = routes or {}
        self.fail = fail
        self.calls: list[tuple[str, str]] = []

    def request(self, method, url, *, headers=None, json=None, data=None, timeout=30.0):
        path = url.split("/", 3)[-1] if "://" in url else url
        self.calls.append((method, path))

        if self.fail:
            raise OSError("connection refused")

        for fragment, (status, body) in self.routes.items():
            if fragment in url:
                payload = body if isinstance(body, bytes) else json_bytes(body)

                return HttpResponse(status=status, body=payload)

        return HttpResponse(status=404, body=b"{}")


def json_bytes(body: object) -> bytes:
    return json.dumps(body).encode()


def evolution(routes=None, fail=False, base_url="https://evo.test", instance="taxpilot"):
    return EvolutionProvider(
        base_url=base_url,
        instance=instance,
        api_key="k",
        transport=FakeEvolution(routes, fail),
    )


# ── A QR code is a credential ─────────────────────────────────────────────


class TestTheQrCodeIsTreatedAsASecret:
    def test_the_payload_never_appears_in_a_repr(self):
        """The Phase 9 lesson, applied before it can bite: a dataclass repr is
        how secrets reach a log file, and this one links a device."""
        request = LinkRequest(payload="data:image/png;base64,SUPER-SECRET-QR-PAYLOAD")

        assert "SUPER-SECRET-QR-PAYLOAD" not in repr(request)
        assert "SUPER-SECRET-QR-PAYLOAD" not in str(request)
        assert "SUPER-SECRET-QR-PAYLOAD" not in f"{request}"

    def test_the_repr_still_says_whether_a_code_was_issued(self):
        # "A code exists but will not scan" and "no code came back" need
        # different fixes, so the length survives even though the payload does
        # not.
        assert "chars" in repr(LinkRequest(payload="x" * 40))

    def test_an_exception_carrying_one_does_not_leak_it(self):
        request = LinkRequest(payload="SECRET")

        try:
            raise RuntimeError(f"could not show {request!r}")
        except RuntimeError as exc:
            assert "SECRET" not in str(exc)

    def test_it_expires(self):
        issued = datetime.now(UTC) - timedelta(seconds=QR_LIFETIME_SECONDS + 1)

        assert LinkRequest(payload="x", issued_at=issued).is_expired()

    def test_a_fresh_one_has_not(self):
        assert LinkRequest(payload="x").is_expired() is False

    def test_the_window_is_short(self):
        """An unscanned QR sitting in a database is a standing invitation."""
        assert QR_LIFETIME_SECONDS <= 120


# ── What each provider can honestly offer ─────────────────────────────────


class TestWhichProvidersCanBeLinked:
    def test_evolution_can(self):
        assert supports_linking(evolution()) is True

    def test_meta_cannot(self):
        """Not a gap — Meta's Cloud API has no QR flow at all. A Connect button
        drawn for it would offer something that cannot exist."""
        assert supports_linking(MetaBusinessProvider(MetaConfig("PN1", "token"))) is False

    def test_meta_still_reports_a_connection(self):
        # The settings screen has to say something true about a Meta deployment.
        status = status_of(MetaBusinessProvider(MetaConfig("PN1", "token")))

        assert status.state is SessionState.CONNECTED
        assert status.number == "PN1"

    def test_a_provider_without_the_method_is_unknown_not_broken(self):
        """Old builds and test doubles simply do not have it. "We cannot tell"
        beats an AttributeError halfway through rendering a status page."""
        assert status_of(object()).state is SessionState.UNAVAILABLE


# ── Reading the connection state ──────────────────────────────────────────


class TestSessionStatus:
    def test_open_is_connected(self):
        provider = evolution({
            "connectionState": (200, {"instance": {"state": "open"}}),
            "fetchInstances": (200, [{"instance": {"owner": "923001234567@s.whatsapp.net"}}]),
        })

        status = provider.session_status()

        assert status.state is SessionState.CONNECTED
        assert status.is_usable

    def test_the_connected_number_is_reported_back(self):
        """Shown to the customer so they can confirm they linked the account
        they meant to — the intake number, not the firm's main one."""
        provider = evolution({
            "connectionState": (200, {"instance": {"state": "open"}}),
            "fetchInstances": (200, [{"instance": {"owner": "923001234567@s.whatsapp.net"}}]),
        })

        assert provider.session_status().number == "923001234567"

    def test_connecting_means_awaiting_a_scan(self):
        provider = evolution({"connectionState": (200, {"instance": {"state": "connecting"}})})

        assert provider.session_status().state is SessionState.AWAITING_SCAN

    def test_no_instance_is_disconnected_not_broken(self):
        """404 means nobody has connected yet. That is a new installation, not
        a fault."""
        provider = evolution({})

        status = provider.session_status()

        assert status.state is SessionState.DISCONNECTED
        assert status.needs_attention is False

    def test_an_unreachable_provider_is_unavailable_not_disconnected(self):
        """The distinction that decides what somebody does next: "reconnect it"
        against "go and look at Evolution"."""
        status = evolution(fail=True).session_status()

        assert status.state is SessionState.UNAVAILABLE
        assert status.needs_attention is True

    def test_an_unconfigured_deployment_says_so(self):
        status = evolution(base_url="").session_status()

        assert status.state is SessionState.UNAVAILABLE
        assert "no Evolution URL" in status.detail

    def test_an_html_error_page_is_not_read_as_a_state(self):
        provider = evolution({"connectionState": (200, b"<html>login</html>")})

        assert provider.session_status().state is SessionState.UNAVAILABLE


# ── Starting a link ───────────────────────────────────────────────────────


class TestStartingALink:
    def test_it_returns_a_code_to_scan(self):
        provider = evolution({
            "connectionState": (404, {}),
            "instance/create": (201, {"qrcode": {"base64": "data:image/png;base64,AAAA"}}),
        })

        result = provider.start_link()

        assert isinstance(result, LinkRequest)
        assert result.payload.endswith("AAAA")

    def test_an_already_connected_account_is_not_relinked(self):
        """The one that would hurt: issuing a QR starts a fresh link and drops
        the working session. A customer pressing Connect twice must not
        disconnect themselves."""
        provider = evolution({
            "connectionState": (200, {"instance": {"state": "open"}}),
            "fetchInstances": (200, [{"instance": {"owner": "923001234567@s.whatsapp.net"}}]),
        })

        result = provider.start_link()

        assert isinstance(result, SessionStatus)
        assert result.state is SessionState.CONNECTED
        assert not any("instance/create" in path for _, path in provider._http.calls)  # noqa: SLF001

    def test_an_existing_instance_falls_through_to_connect(self):
        """"Already exists" is the ordinary case on a reconnect, not a failure."""
        provider = evolution({
            "connectionState": (404, {}),
            "instance/create": (403, {"message": "already in use"}),
            "instance/connect": (200, {"base64": "data:image/png;base64,BBBB"}),
        })

        result = provider.start_link()

        assert isinstance(result, LinkRequest)
        assert result.payload.endswith("BBBB")

    def test_a_pairing_code_is_accepted_too(self):
        provider = evolution({
            "connectionState": (404, {}),
            "instance/create": (201, {"pairingCode": "ABCD-1234"}),
        })

        assert isinstance(provider.start_link(), LinkRequest)

    def test_an_unreachable_provider_reports_rather_than_raises(self):
        """This answers a person waiting at a screen. An exception here would
        surface as a stack trace instead of "Evolution is down"."""
        result = evolution(fail=True).start_link()

        assert isinstance(result, SessionStatus)
        assert result.state is SessionState.UNAVAILABLE

    def test_an_unconfigured_deployment_cannot_link(self):
        result = evolution(base_url="").start_link()

        assert isinstance(result, SessionStatus)
        assert result.state is SessionState.UNAVAILABLE


# ── Unlinking ─────────────────────────────────────────────────────────────


class TestUnlinking:
    def test_it_logs_out(self):
        provider = evolution({"instance/logout": (200, {"status": "SUCCESS"})})

        assert provider.unlink().state is SessionState.DISCONNECTED

    def test_nothing_to_log_out_of_is_the_state_asked_for(self):
        provider = evolution({})

        assert provider.unlink().state is SessionState.DISCONNECTED

    def test_a_failure_is_reported(self):
        assert evolution(fail=True).unlink().state is SessionState.UNAVAILABLE


# ── What the states mean ──────────────────────────────────────────────────


class TestWhatNeedsAttention:
    @pytest.mark.parametrize(
        "state,attention",
        [
            (SessionState.CONNECTED, False),
            # Never connected is not broken, it is new. Telling somebody their
            # WhatsApp "needs attention" before they have set it up is noise.
            (SessionState.DISCONNECTED, False),
            (SessionState.AWAITING_SCAN, False),
            (SessionState.LOGGED_OUT, True),
            (SessionState.UNAVAILABLE, True),
        ],
    )
    def test_only_a_break_is_worth_reporting(self, state, attention):
        assert SessionStatus(state).needs_attention is attention

    def test_only_connected_is_usable(self):
        for state in SessionState:
            assert SessionStatus(state).is_usable is (state is SessionState.CONNECTED)


class TestLinkExpired:
    """Evolution's `refused`, which is neither an outage nor a logout.

    It is emitted when the QR has rotated QRCODE_LIMIT times (30 by default)
    and nobody scanned any of them, at which point Evolution ends the attempt.
    Found by reading its source; it appears in no documentation.

    Both obvious mappings are wrong, and each is wrong in a way that costs
    somebody time: UNAVAILABLE sends an operator to check a provider that is
    working perfectly, and LOGGED_OUT reports a dropped connection that was
    never made.
    """

    def test_refused_is_its_own_state(self):
        provider = evolution({"connectionState": (200, {"instance": {"state": "refused"}})})

        assert provider.session_status().state is SessionState.LINK_EXPIRED

    def test_it_is_not_an_outage(self):
        provider = evolution({"connectionState": (200, {"instance": {"state": "refused"}})})

        assert provider.session_status().state is not SessionState.UNAVAILABLE

    def test_it_is_not_a_logout(self):
        provider = evolution({"connectionState": (200, {"instance": {"state": "refused"}})})

        assert provider.session_status().state is not SessionState.LOGGED_OUT

    def test_it_never_raises_an_alarm(self):
        """Nothing is broken. Paging an administrator because a customer got
        distracted is how alerts come to be ignored."""
        assert SessionStatus(SessionState.LINK_EXPIRED).needs_attention is False

    def test_it_is_not_usable(self):
        assert SessionStatus(SessionState.LINK_EXPIRED).is_usable is False


class TestRotationCounting:
    """"Code 4 of 30" instead of an invisible countdown."""

    def test_the_rotation_is_reported(self):
        provider = evolution({
            "connectionState": (404, {}),
            "instance/create": (201, {"qrcode": {"base64": "data:image/png;base64,AAAA", "count": 4}}),
        })

        request = provider.start_link()

        assert request.rotation == 4
        assert request.rotation_limit == 30
        assert request.rotations_left == 26

    def test_a_provider_that_does_not_count_says_nothing(self):
        """Absent is reported as absent. Inventing a rotation number would put
        a made-up countdown in front of a customer."""
        provider = evolution({
            "connectionState": (404, {}),
            "instance/create": (201, {"qrcode": {"base64": "data:image/png;base64,AAAA"}}),
        })

        request = provider.start_link()

        assert request.rotation is None
        assert request.rotations_left is None

    def test_the_limit_is_configurable(self):
        # Evolution's QRCODE_LIMIT is not exposed over its API, so it is
        # configured to match whatever the instance is running.
        provider = EvolutionProvider(
            base_url="https://evo.test", instance="t", api_key="k",
            transport=FakeEvolution({
                "connectionState": (404, {}),
                "instance/create": (201, {"qrcode": {"base64": "x", "count": 2}}),
            }),
            qr_rotation_limit=5,
        )

        assert provider.start_link().rotations_left == 3

    def test_the_count_never_goes_negative(self):
        request = LinkRequest(payload="x", rotation=40, rotation_limit=30)

        assert request.rotations_left == 0

    def test_the_rotation_is_safe_to_log(self):
        """The number is useful in a log line. The payload is a credential."""
        rendered = repr(LinkRequest(payload="SECRET", rotation=4, rotation_limit=30))

        assert "SECRET" not in rendered
