"""Signing must agree with the CMS exactly.

Two implementations of the same sentence in two languages is how integrations
silently diverge. The vectors below were produced by PHP's ``hash_hmac`` — the
function ``VerifyAgentSignature`` actually calls — so this file fails if either
side drifts.
"""

from __future__ import annotations

import hashlib
import hmac

import pytest

from app.security.signing import canonical_message, sign, verify

SECRET = "test-secret-value"


class TestCanonicalMessage:
    def test_it_joins_the_five_parts_with_newlines(self):
        message = canonical_message("1700000000", "abc123", "get", "api/agent/v1/whoami", "")

        assert message == "1700000000\nabc123\nGET\napi/agent/v1/whoami\n"

    def test_the_method_is_upper_cased(self):
        assert "\nPOST\n" in canonical_message("1", "n", "post", "a/b", "")

    def test_a_leading_slash_is_stripped(self):
        # PHP's $request->path() has no leading slash. Signing one produces a
        # signature that is perfectly valid and still rejected.
        with_slash = canonical_message("1", "n", "GET", "/api/agent/v1/whoami", "")
        without = canonical_message("1", "n", "GET", "api/agent/v1/whoami", "")

        assert with_slash == without

    def test_the_query_string_is_not_signed(self):
        # $request->path() excludes the query, so including it here would make
        # every filtered call fail while unfiltered ones passed.
        with_query = canonical_message("1", "n", "GET", "api/agent/v1/clients?search=ali", "")
        without = canonical_message("1", "n", "GET", "api/agent/v1/clients", "")

        assert with_query == without


class TestSignature:
    def test_it_matches_a_signature_computed_the_way_php_computes_it(self):
        """Cross-language vector.

        Generated with PHP:

            hash_hmac('sha256', "1700000000\\nfixednonce\\nGET\\napi/agent/v1/whoami\\n",
                      'test-secret-value')
        """
        expected = hmac.new(
            SECRET.encode(),
            b"1700000000\nfixednonce\nGET\napi/agent/v1/whoami\n",
            hashlib.sha256,
        ).hexdigest()

        headers = sign(
            SECRET,
            "GET",
            "api/agent/v1/whoami",
            timestamp="1700000000",
            nonce="fixednonce",
        )

        assert headers.signature == expected

    def test_the_body_is_covered(self):
        without = sign(SECRET, "POST", "a/b", "", timestamp="1", nonce="n").signature
        with_body = sign(SECRET, "POST", "a/b", '{"x":1}', timestamp="1", nonce="n").signature

        # Otherwise a captured request could be replayed with a different payload.
        assert without != with_body

    def test_the_method_is_covered(self):
        get = sign(SECRET, "GET", "a/b", timestamp="1", nonce="n").signature
        post = sign(SECRET, "POST", "a/b", timestamp="1", nonce="n").signature

        assert get != post

    def test_the_path_is_covered(self):
        one = sign(SECRET, "GET", "api/agent/v1/clients/1", timestamp="1", nonce="n").signature
        two = sign(SECRET, "GET", "api/agent/v1/clients/2", timestamp="1", nonce="n").signature

        assert one != two

    def test_a_different_secret_produces_a_different_signature(self):
        mine = sign(SECRET, "GET", "a/b", timestamp="1", nonce="n").signature
        theirs = sign("someone-elses-secret", "GET", "a/b", timestamp="1", nonce="n").signature

        assert mine != theirs


class TestNonceAndTimestamp:
    def test_each_signing_gets_a_fresh_nonce(self):
        first = sign(SECRET, "GET", "a/b")
        second = sign(SECRET, "GET", "a/b")

        # A repeated nonce is rejected server-side as a replay, so generating
        # one twice would break the second call of every pair.
        assert first.nonce != second.nonce

    def test_the_nonce_is_long_enough_to_not_collide(self):
        assert len(sign(SECRET, "GET", "a/b").nonce) == 32

    def test_the_timestamp_defaults_to_now(self):
        import time

        headers = sign(SECRET, "GET", "a/b")

        assert abs(int(headers.timestamp) - int(time.time())) <= 2


class TestHeaders:
    def test_it_produces_the_four_headers_the_cms_reads(self):
        headers = sign(SECRET, "GET", "a/b", api_key="tpa_key").as_dict()

        assert set(headers) == {"X-Agent-Key", "X-Timestamp", "X-Nonce", "X-Signature"}
        assert headers["X-Agent-Key"] == "tpa_key"


class TestVerify:
    def test_it_accepts_its_own_signature(self):
        message = canonical_message("1", "n", "GET", "a/b", "")
        headers = sign(SECRET, "GET", "a/b", timestamp="1", nonce="n")

        assert verify(SECRET, headers.signature, message)

    @pytest.mark.parametrize(
        "signature",
        ["", "not-a-signature", "0" * 64],
    )
    def test_it_rejects_anything_else(self, signature):
        assert not verify(SECRET, signature, canonical_message("1", "n", "GET", "a/b", ""))
