"""The release signature, and the product boundary it defends.

Two independent sources of truth, deliberately:

  * `tests/fixtures/release_signatures.json` holds signatures made by OpenSSL,
    through PHP, with the real 4096-bit release key. If the hand-written
    verifier in app/release/signing.py ever disagrees with OpenSSL, these fail.
  * `tests/rsa_support.py` generates a keypair with an independently written DER
    encoder, so the parser is exercised against something that did not come from
    the same code path.

Neither alone is enough. The fixture proves agreement with a real
implementation; the generated key proves the parser is not just tolerating one
particular byte layout it was written against.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from app.release.manifest import (
    MANIFEST_PREFIX,
    ManifestError,
    Signature,
    parse_signing_string,
    signing_string,
)
from app.release.signing import SignatureError, load_public_key, verify

FIXTURE = Path(__file__).parent / "fixtures" / "release_signatures.json"


@pytest.fixture(scope="module")
def openssl():
    return json.loads(FIXTURE.read_text(encoding="utf-8"))


class TestAgreesWithOpenSSL:
    """Signatures made by the real release key, verified here."""

    def test_every_fixture_case_agrees(self, openssl):
        key = openssl["public_key"]

        for case in openssl["cases"]:
            result = verify(case["manifest"], case["signature"], key)

            assert result is case["verifies"], case["name"]

    def test_the_release_key_parses(self, openssl):
        parsed = load_public_key(openssl["public_key"])

        assert parsed.size * 8 == 4096
        assert parsed.exponent == 65537

    def test_a_signature_from_another_release_does_not_transfer(self, openssl):
        """The replay this design exists to stop.

        A signature is only ever valid for the exact string it was made over,
        and the version is inside that string — so an older, legitimately
        signed, vulnerable release cannot be served as though it were the
        newest one.
        """
        replayed = next(c for c in openssl["cases"] if "replayed" in c["name"])

        assert verify(replayed["manifest"], replayed["signature"], openssl["public_key"]) is False


class TestTheProductBoundary:
    """A CMS package must never install as an AI release, or the reverse.

    Both are signed by the same offline key, so the signature alone cannot tell
    them apart — and the CMS installer extracts over a live application
    directory and runs `migrate --force`. The prefix is the only thing between
    those two facts.
    """

    def test_a_cms_signature_is_genuine_and_still_refused(self, openssl):
        case = next(c for c in openssl["cases"] if c.get("wrong_product"))

        # It really is signed by the release key. That is the point: the
        # signature check passes, and the release is rejected anyway.
        assert verify(case["manifest"], case["signature"], openssl["public_key"]) is True

        with pytest.raises(ManifestError, match="not a TaxPilot AI release"):
            parse_signing_string(case["manifest"])

    def test_the_two_prefixes_are_not_the_same(self):
        assert MANIFEST_PREFIX == "taxpilot-ai-release-v1"
        assert MANIFEST_PREFIX != "taxpilot-update-v1"


class TestVerification:
    """Behaviour against a key generated for this test run."""

    def test_a_signature_it_made_verifies(self, test_key):
        message = signing_string("1.2.0", "ab" * 32, 4096)

        assert verify(message, test_key.sign(message), test_key.public_pem) is True

    def test_a_signature_from_a_different_key_does_not(self, test_key, other_key):
        message = signing_string("1.2.0", "ab" * 32, 4096)

        assert verify(message, other_key.sign(message), test_key.public_pem) is False

    @pytest.mark.parametrize(
        "signature",
        [
            "",
            "   ",
            "not base64 at all!!",
            "YWJj",  # valid base64, far too short for a 2048-bit modulus
        ],
        ids=["empty", "blank", "not-base64", "too-short"],
    )
    def test_unusable_signatures_are_refused_not_raised(self, test_key, signature):
        """Refused, not raised.

        A corrupted signature is the case this exists to reject. If it came back
        as an exception, the caller distinguishing "broken deployment" from
        "someone tampered with the package" would get the wrong answer for the
        more dangerous of the two.
        """
        assert verify("anything", signature, test_key.public_pem) is False

    def test_a_signature_longer_than_the_modulus_is_refused(self, test_key):
        import base64

        oversized = base64.b64encode(b"\xff" * (test_key.size + 1)).decode()

        assert verify("anything", oversized, test_key.public_pem) is False


class TestKeyLoading:
    def test_an_unparseable_key_raises_rather_than_returning_false(self):
        """"Cannot verify" must never look like "did not verify".

        The first is a broken installation and the second is an attack. An
        operator seeing the wrong one of those looks in entirely the wrong
        place.
        """
        with pytest.raises(SignatureError):
            verify("message", "c2ln", "-----BEGIN PUBLIC KEY-----\nnonsense\n-----END PUBLIC KEY-----")

    def test_an_empty_key_raises(self):
        with pytest.raises(SignatureError, match="empty"):
            load_public_key("")

    def test_a_truncated_key_raises(self, test_key):
        pem = test_key.public_pem
        body = "".join(pem.splitlines()[1:-1])
        truncated = f"-----BEGIN PUBLIC KEY-----\n{body[: len(body) // 2]}\n-----END PUBLIC KEY-----"

        with pytest.raises(SignatureError):
            load_public_key(truncated)

    def test_a_small_key_is_refused(self):
        """1024 bits verifies fine mathematically and is not acceptable.

        A weak key that loads is worse than one that does not: it produces
        signatures that check out, and everything downstream then reports the
        release as verified.
        """
        from tests.rsa_support import generate

        weak = generate(bits=1024, seed=7)

        with pytest.raises(SignatureError, match="too small"):
            load_public_key(weak.public_pem)


class TestTheSignedString:
    def test_it_is_built_the_same_way_every_time(self):
        assert signing_string(" 1.2.0 ", "AB" * 32, 4096) == signing_string("1.2.0", "ab" * 32, 4096)

    def test_it_carries_the_version_digest_and_size(self):
        assert signing_string("1.2.0", "ab" * 32, 4096) == f"{MANIFEST_PREFIX}|1.2.0|{'ab' * 32}|4096"

    def test_it_round_trips(self):
        parsed = parse_signing_string(signing_string("1.2.0", "cd" * 32, 512))

        assert (parsed.version, parsed.sha256, parsed.size) == ("1.2.0", "cd" * 32, 512)

    @pytest.mark.parametrize(
        "raw",
        [
            "taxpilot-ai-release-v1|1.2.0|short|4096",
            "taxpilot-ai-release-v1|not-a-version|" + "ab" * 32 + "|4096",
            "taxpilot-ai-release-v1|1.2.0|" + "ab" * 32 + "|not-a-number",
            "taxpilot-ai-release-v1|1.2.0|" + "ab" * 32,
            "",
        ],
        ids=["short-digest", "bad-version", "bad-size", "missing-field", "empty"],
    )
    def test_a_malformed_string_is_refused(self, raw):
        with pytest.raises(ManifestError):
            parse_signing_string(raw)


class TestTheSignatureFile:
    def test_it_round_trips(self):
        original = Signature(manifest=signing_string("1.0.0", "ab" * 32, 10), signature="c2ln")

        assert Signature.from_json(original.to_json()) == original

    @pytest.mark.parametrize(
        "raw",
        ['{"manifest": ""}', '{"signature": "x"}', "[]", "not json", "{}"],
        ids=["no-signature", "no-manifest", "not-object", "not-json", "empty"],
    )
    def test_an_incomplete_signature_file_is_refused(self, raw):
        with pytest.raises(ManifestError):
            Signature.from_json(raw)
