"""Building a release archive.

The property that matters most here is reproducibility. The signature attests to
a digest, so "is the thing you signed the thing in the repository?" is only
answerable if building the same tree twice produces the same bytes.
"""

from __future__ import annotations

import gzip
import tarfile
from pathlib import Path

import pytest

from app.release import package
from app.release.manifest import MANIFEST_NAME, Manifest
from app.release.version import VERSION, at_least, is_valid, parts


@pytest.fixture
def source(tmp_path: Path) -> Path:
    """A miniature project tree with everything build() looks at."""
    root = tmp_path / "src"
    (root / "app" / "database" / "migrations").mkdir(parents=True)
    (root / "app" / "__pycache__").mkdir()

    (root / "pyproject.toml").write_text('requires-python = ">=3.12"\n', encoding="utf-8")
    (root / "README.md").write_text("# test\n", encoding="utf-8")
    (root / "app" / "__init__.py").write_text("", encoding="utf-8")
    (root / "app" / "thing.py").write_text("VALUE = 1\n", encoding="utf-8")
    (root / "app" / "database" / "migrations" / "0001_first.sql").write_text("SELECT 1;", encoding="utf-8")
    (root / "app" / "database" / "migrations" / "0002_second.sql").write_text("SELECT 2;", encoding="utf-8")

    # Things that must never ship.
    (root / ".env").write_text("TAXPILOT_AGENT_API_SECRET=sk-live-secret\n", encoding="utf-8")
    (root / "app" / "__pycache__" / "thing.cpython-312.pyc").write_bytes(b"\x00\x01")
    (root / "app" / "debug.log").write_text("noise", encoding="utf-8")

    return root


def names_in(archive: Path) -> set[str]:
    with tarfile.open(archive, "r:gz") as tar:
        return set(tar.getnames())


class TestWhatShips:
    def test_the_application_ships(self, source, tmp_path):
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        assert "app/thing.py" in names_in(artifact.path)
        assert "pyproject.toml" in names_in(artifact.path)

    def test_the_manifest_is_inside(self, source, tmp_path):
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        assert MANIFEST_NAME in names_in(artifact.path)

    def test_the_env_file_never_ships(self, source, tmp_path):
        """The one that matters.

        Configuration lives in the deployment's `shared/`, and a release archive
        containing a `.env` would distribute the agent secret to wherever the
        archive goes.
        """
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        assert not any(name.endswith(".env") for name in names_in(artifact.path))

    def test_build_artefacts_never_ship(self, source, tmp_path):
        contents = names_in(package.build(source, "1.0.0", tmp_path / "dist").path)

        assert not any("__pycache__" in name for name in contents)
        assert not any(name.endswith(".pyc") for name in contents)
        assert not any(name.endswith(".log") for name in contents)

    def test_it_refuses_an_empty_tree(self, tmp_path):
        empty = tmp_path / "empty"
        empty.mkdir()

        with pytest.raises(package.PackageError, match="Nothing to package"):
            package.build(empty, "1.0.0", tmp_path / "dist")

    def test_it_refuses_a_version_that_is_not_one(self, source, tmp_path):
        with pytest.raises(package.PackageError, match="Not a release version"):
            package.build(source, "latest", tmp_path / "dist")


class TestTheManifest:
    def test_it_records_the_migrations_in_the_tree(self, source, tmp_path):
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        assert artifact.manifest.migrations == ("0001_first.sql", "0002_second.sql")

    def test_it_reads_the_python_floor_from_pyproject(self, source, tmp_path):
        """Parsed rather than hardcoded, so a package cannot disagree with the
        project it was built from."""
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        assert artifact.manifest.requires_python == "3.12.0"

    def test_it_carries_the_cms_floor_when_given_one(self, source, tmp_path):
        artifact = package.build(source, "1.0.0", tmp_path / "dist", requires_cms="1.1.3")

        assert artifact.manifest.requires_cms == "1.1.3"

    def test_the_manifest_inside_matches_the_artifact(self, source, tmp_path):
        artifact = package.build(source, "1.2.3", tmp_path / "dist")

        with tarfile.open(artifact.path, "r:gz") as tar:
            member = tar.extractfile(MANIFEST_NAME)
            stored = Manifest.from_json(member.read())

        assert stored.version == artifact.version == "1.2.3"


class TestReproducibility:
    def test_the_same_tree_builds_the_same_bytes(self, source, tmp_path):
        """Without this, "I signed the code in the repository" is unverifiable."""
        first = package.build(source, "1.0.0", tmp_path / "a")
        second = package.build(source, "1.0.0", tmp_path / "b")

        assert first.sha256 == second.sha256
        assert first.size == second.size

    def test_a_changed_file_changes_the_digest(self, source, tmp_path):
        first = package.build(source, "1.0.0", tmp_path / "a")
        (source / "app" / "thing.py").write_text("VALUE = 2\n", encoding="utf-8")
        second = package.build(source, "1.0.0", tmp_path / "b")

        assert first.sha256 != second.sha256

    def test_the_gzip_header_carries_no_timestamp(self, source, tmp_path):
        """gzip stores an mtime of its own, which `tarfile.open(mode="w:gz")`
        fills in from the clock — enough on its own to make every build
        different."""
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        with artifact.path.open("rb") as handle:
            header = handle.read(8)

        assert int.from_bytes(header[4:8], "little") == 0

    def test_members_carry_no_owner_or_build_time(self, source, tmp_path):
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        with tarfile.open(artifact.path, "r:gz") as tar:
            for member in tar.getmembers():
                assert member.uid == member.gid == 0
                assert member.uname == member.gname == ""
                assert member.mtime == package.FIXED_MTIME

    def test_it_is_a_real_gzip_stream(self, source, tmp_path):
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        with gzip.open(artifact.path, "rb") as handle:
            assert handle.read(1)


class TestTheArtifact:
    def test_it_knows_what_has_to_be_signed(self, source, tmp_path):
        artifact = package.build(source, "1.4.0", tmp_path / "dist")

        assert artifact.signing_string == f"taxpilot-ai-release-v1|1.4.0|{artifact.sha256}|{artifact.size}"

    def test_the_signature_sits_beside_the_archive(self, source, tmp_path):
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        assert artifact.signature_path.name == "taxpilot-ai-1.0.0.tar.gz.sig"

    def test_the_digest_is_of_the_file_on_disk(self, source, tmp_path):
        artifact = package.build(source, "1.0.0", tmp_path / "dist")

        assert package.sha256_of(artifact.path) == artifact.sha256


class TestVersions:
    def test_pyproject_and_the_code_agree(self):
        """Two copies of a fact drift. This is what stops them.

        Neither file is the authority — they are simply required to match, so a
        release cannot be built whose manifest says one thing and whose
        packaging metadata says another.
        """
        pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml"
        declared = [
            line.split("=", 1)[1].strip().strip('"')
            for line in pyproject.read_text(encoding="utf-8").splitlines()
            if line.strip().startswith("version")
        ]

        assert declared and declared[0] == VERSION

    @pytest.mark.parametrize(
        "version,valid",
        [
            ("1.0.0", True),
            ("10.20.30", True),
            ("1.2.0-rc1", True),
            ("1.2", False),
            ("v1.2.0", False),
            ("latest", False),
            ("", False),
        ],
    )
    def test_what_counts_as_a_version(self, version, valid):
        assert is_valid(version) is valid

    def test_a_pre_release_compares_as_its_base(self):
        """1.2.0-rc1 satisfies "at least 1.2.0" — which is why pre-releases
        belong on their own channel rather than being ordered against stable
        ones."""
        assert parts("1.2.0-rc1") == (1, 2, 0)
        assert at_least("1.2.0-rc1", "1.2.0") is True

    def test_ordering(self):
        assert at_least("1.10.0", "1.9.0") is True
        assert at_least("1.9.0", "1.10.0") is False
        assert at_least("2.0.0", "2.0.0") is True
