import pathlib

import pytest
from click.testing import CliRunner

import sqlite_utils
import sqlite_utils.cli

TWO_MIGRATIONS = """
from sqlite_utils import Migrations

m = Migrations("hello")

@m()
def foo(db):
    db.table("foo").insert({"hello": "world"})

@m()
def bar(db):
    db.table("bar").insert({"hello": "world"})
"""


@pytest.fixture
def two_migrations(tmpdir):
    path = pathlib.Path(tmpdir)
    (path / "foo").mkdir()
    migrations_py = path / "foo" / "migrations.py"
    migrations_py.write_text(TWO_MIGRATIONS, "utf-8")
    return path, migrations_py


@pytest.fixture
def two_sets_same_migration_name(tmpdir):
    path = pathlib.Path(tmpdir)
    migrations_py = path / "migrations.py"
    migrations_py.write_text(
        """
from sqlite_utils import Migrations

creatures = Migrations("creatures")

@creatures()
def create_table(db):
    db.table("creatures").insert({"name": "Cleo"})

@creatures()
def add_weight(db):
    db.table("creature_weights").insert({"weight": 4.2})

sales = Migrations("sales")

@sales()
def create_table(db):
    db.table("sales").insert({"id": 1})

@sales()
def add_weight(db):
    db.table("sales_weights").insert({"weight": 10})
""",
        "utf-8",
    )
    return path, migrations_py


@pytest.mark.parametrize("arg", ("TMPDIR", "TMPDIR/foo/migrations.py", "TMPDIR/foo/"))
def test_basic(two_migrations, arg):
    path, _ = two_migrations
    db_path = str(path / "test.db")

    runner = CliRunner()

    def _list():
        list_result = runner.invoke(
            sqlite_utils.cli.cli,
            ["migrate", db_path, "--list", arg.replace("TMPDIR", str(path))],
        )
        assert list_result.exit_code == 0
        return list_result.output

    assert _list() == (
        "Migrations for: hello\n\n"
        "  Applied:\n\n"
        "  Pending:\n"
        "    foo\n"
        "    bar\n\n"
    )

    result = runner.invoke(
        sqlite_utils.cli.cli, ["migrate", db_path, arg.replace("TMPDIR", str(path))]
    )
    assert result.exit_code == 0, result.output

    list_output = _list()
    assert "Migrations for: hello\n\n  Applied:\n    " in list_output
    prior_to_pending = list_output.split(" Pending")[0]
    assert "  foo" in prior_to_pending
    assert "  bar" in prior_to_pending
    assert " Pending:\n    (none)" in list_output

    db = sqlite_utils.Database(db_path)
    assert db.table("foo").exists()
    assert db.table("bar").exists()
    assert db.table("_sqlite_migrations").exists()
    rows = list(db.table("_sqlite_migrations").rows)
    assert len(rows) == 2
    assert rows[0]["name"] == "foo"
    assert rows[1]["name"] == "bar"


def test_list_same_migration_names_in_different_sets(capsys):
    applied = sqlite_utils.Migrations("applied")

    @applied(name="foo")
    def applied_foo(db):
        db.table("applied").insert({"hello": "world"})

    pending = sqlite_utils.Migrations("pending")

    @pending(name="foo")
    def pending_foo(db):
        db.table("pending").insert({"hello": "world"})

    db = sqlite_utils.Database(memory=True)
    applied.apply(db)

    sqlite_utils.cli._display_migration_list(db, [applied, pending])

    output = capsys.readouterr().out
    assert (
        "Migrations for: pending\n\n" "  Applied:\n\n" "  Pending:\n" "    foo\n\n"
    ) in output


def test_verbose(tmpdir):
    path = pathlib.Path(tmpdir)
    (path / "foo").mkdir()
    migrations_py = path / "foo" / "migrations.py"
    migrations_py.write_text(
        """
from sqlite_utils import Migrations

m = Migrations("hello")

@m()
def foo(db):
    db.table("dogs").insert({"id": 1, "name": "Cleo"})
    """,
        "utf-8",
    )
    db_path = str(path / "test.db")
    runner = CliRunner()
    result = runner.invoke(
        sqlite_utils.cli.cli, ["migrate", db_path, str(migrations_py)]
    )
    assert result.exit_code == 0

    result = runner.invoke(
        sqlite_utils.cli.cli, ["migrate", db_path, str(migrations_py), "--verbose"]
    )
    assert result.exit_code == 0
    expected = """
Schema before:

  CREATE TABLE "_sqlite_migrations" (
     "id" INTEGER PRIMARY KEY,
     "migration_set" TEXT,
     "name" TEXT,
     "applied_at" TEXT
  );
  CREATE UNIQUE INDEX "idx__sqlite_migrations_migration_set_name"
      ON "_sqlite_migrations" ("migration_set", "name");
  CREATE TABLE "dogs" (
     "id" INTEGER,
     "name" TEXT
  );

Schema after:

  (unchanged)
""".strip()
    assert expected in result.output

    new_migration = """
@m()
def bar(db):
    db.table("dogs").add_column("age", int)
    db.table("dogs").add_column("weight", float)
    db.table("dogs").transform()
"""
    migrations_py.write_text(migrations_py.read_text("utf-8") + new_migration)

    result = runner.invoke(
        sqlite_utils.cli.cli, ["migrate", db_path, str(migrations_py), "--verbose"]
    )
    assert result.exit_code == 0
    expected_diff = """
Schema diff:

     ON "_sqlite_migrations" ("migration_set", "name");
 CREATE TABLE "dogs" (
    "id" INTEGER,
-   "name" TEXT
+   "name" TEXT,
+   "age" INTEGER,
+   "weight" REAL
 );
""".strip()
    assert expected_diff in result.output


def test_stop_before(two_migrations):
    path, _ = two_migrations
    db_path = str(path / "test.db")
    result = CliRunner().invoke(
        sqlite_utils.cli.cli,
        [
            "migrate",
            db_path,
            str(path / "foo" / "migrations.py"),
            "--stop-before",
            "bar",
        ],
    )
    assert result.exit_code == 0
    db = sqlite_utils.Database(db_path)
    assert db.table("foo").exists()
    assert not db.table("bar").exists()


def test_stop_before_multiple_sets_unqualified(two_migrations):
    path, _ = two_migrations
    db_path = str(path / "test.db")
    (path / "foo" / "migrations2.py").write_text(
        """
from sqlite_utils import Migrations

m = Migrations("hello2")

@m()
def foo(db):
    db.table("foo").insert({"hello": "world"})
    """,
        "utf-8",
    )
    result = CliRunner().invoke(
        sqlite_utils.cli.cli,
        [
            "migrate",
            db_path,
            str(path / "foo" / "migrations.py"),
            str(path / "foo" / "migrations2.py"),
            "--stop-before",
            "foo",
        ],
    )
    assert result.exit_code == 0, result.output
    db = sqlite_utils.Database(db_path)
    assert db.table_names() == ["_sqlite_migrations"]
    assert list(db.table("_sqlite_migrations").rows) == []


def test_stop_before_qualified_only_affects_named_set(two_sets_same_migration_name):
    path, migrations_py = two_sets_same_migration_name
    db_path = str(path / "test.db")
    result = CliRunner().invoke(
        sqlite_utils.cli.cli,
        [
            "migrate",
            db_path,
            str(migrations_py),
            "--stop-before",
            "creatures:add_weight",
        ],
    )
    assert result.exit_code == 0, result.output
    db = sqlite_utils.Database(db_path)
    assert db.table("creatures").exists()
    assert not db.table("creature_weights").exists()
    assert db.table("sales").exists()
    assert db.table("sales_weights").exists()


def test_stop_before_multiple_qualified(two_sets_same_migration_name):
    path, migrations_py = two_sets_same_migration_name
    db_path = str(path / "test.db")
    result = CliRunner().invoke(
        sqlite_utils.cli.cli,
        [
            "migrate",
            db_path,
            str(migrations_py),
            "--stop-before",
            "creatures:add_weight",
            "--stop-before",
            "sales:add_weight",
        ],
    )
    assert result.exit_code == 0, result.output
    db = sqlite_utils.Database(db_path)
    assert db.table("creatures").exists()
    assert not db.table("creature_weights").exists()
    assert db.table("sales").exists()
    assert not db.table("sales_weights").exists()


LEGACY_MIGRATIONS = """
import datetime

class _Migration:
    def __init__(self, name, fn):
        self.name = name
        self.fn = fn

class _Applied:
    def __init__(self, name, applied_at):
        self.name = name
        self.applied_at = applied_at

class LegacyMigrations:
    # Mimics the sqlite-migrate 0.x Migrations class, in particular
    # apply(db, stop_before=None) taking a single string
    migrations_table = "_sqlite_migrations"

    def __init__(self, name):
        self.name = name
        self._migrations = []

    def __call__(self, fn):
        self._migrations.append(_Migration(fn.__name__, fn))
        return fn

    def ensure_migrations_table(self, db):
        db.table(self.migrations_table).create(
            {"migration_set": str, "name": str, "applied_at": str},
            pk=("migration_set", "name"),
            if_not_exists=True,
        )

    def applied(self, db):
        self.ensure_migrations_table(db)
        return [
            _Applied(row["name"], row["applied_at"])
            for row in db.table(self.migrations_table).rows_where(
                "migration_set = ?", [self.name]
            )
        ]

    def pending(self, db):
        applied = {m.name for m in self.applied(db)}
        return [m for m in self._migrations if m.name not in applied]

    def apply(self, db, stop_before=None):
        for migration in self.pending(db):
            if migration.name == stop_before:
                return
            migration.fn(db)
            db.table(self.migrations_table).insert(
                {
                    "migration_set": self.name,
                    "name": migration.name,
                    "applied_at": str(
                        datetime.datetime.now(datetime.timezone.utc)
                    ),
                }
            )

legacy = LegacyMigrations("legacy_set")

@legacy
def first(db):
    db.table("first").insert({"hello": "world"})

@legacy
def second(db):
    db.table("second").insert({"hello": "world"})
"""


def test_stop_before_unknown_name_errors(two_migrations):
    path, _ = two_migrations
    db_path = str(path / "test.db")
    runner = CliRunner()
    result = runner.invoke(
        sqlite_utils.cli.cli,
        ["migrate", db_path, str(path), "--stop-before", "fooo"],
    )
    assert result.exit_code == 1
    assert "--stop-before did not match any migrations: fooo" in result.output
    # Nothing should have been applied
    db = sqlite_utils.Database(db_path)
    assert "foo" not in db.table_names()
    assert "bar" not in db.table_names()


def test_stop_before_with_legacy_migrations_class(tmpdir):
    path = pathlib.Path(tmpdir)
    (path / "migrations.py").write_text(LEGACY_MIGRATIONS, "utf-8")
    db_path = str(path / "test.db")
    runner = CliRunner()
    result = runner.invoke(
        sqlite_utils.cli.cli,
        ["migrate", db_path, str(path), "--stop-before", "second"],
    )
    assert result.exit_code == 0, result.output
    db = sqlite_utils.Database(db_path)
    assert "first" in db.table_names()
    assert "second" not in db.table_names()


def test_stop_before_multiple_values_for_legacy_set_errors(tmpdir):
    path = pathlib.Path(tmpdir)
    (path / "migrations.py").write_text(LEGACY_MIGRATIONS, "utf-8")
    db_path = str(path / "test.db")
    runner = CliRunner()
    result = runner.invoke(
        sqlite_utils.cli.cli,
        [
            "migrate",
            db_path,
            str(path),
            "--stop-before",
            "legacy_set:first",
            "--stop-before",
            "legacy_set:second",
        ],
    )
    assert result.exit_code == 1
    assert "single --stop-before" in result.output


def test_list_does_not_create_database_file(two_migrations):
    path, _ = two_migrations
    db_path = path / "test.db"
    runner = CliRunner()
    result = runner.invoke(
        sqlite_utils.cli.cli, ["migrate", str(db_path), str(path), "--list"]
    )
    assert result.exit_code == 0, result.output
    assert "Pending:\n    foo\n    bar" in result.output
    # Listing migrations must not create the database file
    assert not db_path.exists()


def test_list_does_not_upgrade_legacy_migrations_table(two_migrations):
    path, _ = two_migrations
    db_path = str(path / "test.db")
    db = sqlite_utils.Database(db_path)
    db.table("_sqlite_migrations").create(
        {"migration_set": str, "name": str, "applied_at": str},
        pk=("migration_set", "name"),
    )
    db.table("_sqlite_migrations").insert(
        {"migration_set": "hello", "name": "foo", "applied_at": "x"}
    )
    db.close()
    runner = CliRunner()
    result = runner.invoke(
        sqlite_utils.cli.cli, ["migrate", db_path, str(path), "--list"]
    )
    assert result.exit_code == 0, result.output
    assert "foo - x" in result.output
    # --list must not perform the one-way legacy schema upgrade
    db2 = sqlite_utils.Database(db_path)
    assert db2.table("_sqlite_migrations").pks == ["migration_set", "name"]
    db2.close()


def test_stop_before_applied_migration_errors(two_migrations):
    path, _ = two_migrations
    db_path = str(path / "test.db")
    migrations_path = str(path / "foo" / "migrations.py")
    # Apply everything first
    first = CliRunner().invoke(
        sqlite_utils.cli.cli,
        ["migrate", db_path, migrations_path, "--stop-before", "bar"],
    )
    assert first.exit_code == 0
    # foo is now applied - stopping before it is an error, and bar
    # must not be applied as a side effect
    result = CliRunner().invoke(
        sqlite_utils.cli.cli,
        ["migrate", db_path, migrations_path, "--stop-before", "foo"],
    )
    assert result.exit_code != 0
    assert "already been applied" in result.output
    db = sqlite_utils.Database(db_path)
    assert not db.table("bar").exists()


def test_list_with_legacy_class_is_read_only(tmpdir):
    # Legacy sqlite-migrate classes create the _sqlite_migrations table
    # from their pending()/applied() methods - --list must roll that
    # back so it stays a read-only operation as documented
    path = pathlib.Path(tmpdir)
    (path / "migrations.py").write_text(LEGACY_MIGRATIONS, "utf-8")
    db_path = str(path / "test.db")
    db = sqlite_utils.Database(db_path)
    db.table("existing").insert({"id": 1})
    db.close()
    result = CliRunner().invoke(
        sqlite_utils.cli.cli, ["migrate", db_path, str(path), "--list"]
    )
    assert result.exit_code == 0, result.output
    assert "first" in result.output
    db2 = sqlite_utils.Database(db_path)
    assert "_sqlite_migrations" not in db2.table_names()
    db2.close()
