"""Applying schema changes in order, exactly once.

Discovery, ordering and the checksum rule need no database, so they are tested
properly here. What is not covered is the SQL itself — that needs a server, and
the contract suite runs it when TAXPILOT_TEST_DSN is set.
"""

from __future__ import annotations

import pytest

from app.database.migrator import Migration, MigrationError, discover, pending


def write(directory, name: str, sql: str = "SELECT 1;") -> None:
    (directory / name).write_text(sql, encoding="utf-8")


class TestDiscovery:
    def test_migrations_are_found_and_read(self, tmp_path):
        write(tmp_path, "0001_first.sql", "CREATE TABLE a ();")

        found = discover(tmp_path)

        assert [m.label for m in found] == ["0001_first"]
        assert found[0].sql == "CREATE TABLE a ();"

    def test_they_are_ordered_numerically_not_alphabetically(self, tmp_path):
        for name in ("0002_second.sql", "0010_tenth.sql", "0001_first.sql"):
            write(tmp_path, name)

        # String sort puts 0010 after 0002 only while the padding holds. Sorting
        # on the number means it keeps working the day somebody writes 10_.
        assert [m.version for m in discover(tmp_path)] == ["0001", "0002", "0010"]

    def test_a_misnamed_file_is_an_error_not_a_skip(self, tmp_path):
        write(tmp_path, "0001_first.sql")
        write(tmp_path, "add-index.sql")

        # Skipping it silently means a schema change that ran nowhere, on one
        # installation, and was never noticed until something else broke.
        with pytest.raises(MigrationError, match="add-index.sql"):
            discover(tmp_path)

    def test_two_files_at_the_same_version_are_refused(self, tmp_path):
        write(tmp_path, "0001_first.sql")
        write(tmp_path, "0001_also_first.sql")

        # They have no defined order between them, so which ran first would
        # depend on the filesystem.
        with pytest.raises(MigrationError, match="share version 0001"):
            discover(tmp_path)

    def test_hidden_files_and_directories_are_ignored(self, tmp_path):
        write(tmp_path, "0001_first.sql")
        (tmp_path / ".DS_Store").write_text("junk", encoding="utf-8")
        (tmp_path / "archive").mkdir()

        assert len(discover(tmp_path)) == 1

    def test_a_missing_directory_says_so(self, tmp_path):
        with pytest.raises(MigrationError, match="No migrations directory"):
            discover(tmp_path / "nope")

    def test_the_real_migrations_are_valid(self):
        """The ones that actually ship."""
        assert [m.label for m in discover()] == [
            "0001_workflow_runs",
            "0002_pending_pdfs",
            "0003_intake_outbox",
            "0004_pending_pdf_intake_reference",
            "0005_intake_outbox_claims",
        ]


class TestPending:
    def test_nothing_applied_means_everything_is_pending(self):
        migrations = [Migration("0001", "a", "x"), Migration("0002", "b", "y")]

        assert [m.version for m in pending(migrations, {})] == ["0001", "0002"]

    def test_applied_migrations_are_not_repeated(self):
        first = Migration("0001", "a", "x")
        second = Migration("0002", "b", "y")

        result = pending([first, second], {"0001": first.checksum})

        assert [m.version for m in result] == ["0002"]

    def test_an_edited_migration_is_refused(self):
        migration = Migration("0001", "a", "CREATE TABLE a ();")

        # Somebody changed a file that has already run somewhere. The result is
        # two installations with the same version number and different schemas,
        # and nothing afterwards can tell them apart.
        with pytest.raises(MigrationError, match="has changed since it was applied"):
            pending([migration], {"0001": "a-different-checksum"})

    def test_the_error_says_what_to_do_instead(self):
        migration = Migration("0001", "a", "x")

        with pytest.raises(MigrationError, match="Add a new migration"):
            pending([migration], {"0001": "stale"})

    def test_the_checksum_follows_the_content(self):
        assert Migration("0001", "a", "x").checksum != Migration("0001", "a", "y").checksum
        assert Migration("0001", "a", "x").checksum == Migration("0001", "a", "x").checksum
