import collections
import datetime
import decimal
import json
import pathlib
import uuid

import pytest

from sqlite_utils import ANY
from sqlite_utils.db import (
    AlterError,
    Database,
    DescIndex,
    ForeignKey,
    Index,
    InvalidColumns,
    NoObviousTable,
    NoTable,
    NoView,
    OperationalError,
    Table,
    View,
)
from sqlite_utils.utils import hash_record, sqlite3

try:
    import pandas as pd  # type: ignore
except ImportError:
    pd = None  # type: ignore


def test_create_table(fresh_db):
    assert [] == fresh_db.table_names()
    table = fresh_db.create_table(
        "test_table",
        {
            "text_col": str,
            "float_col": float,
            "int_col": int,
            "bool_col": bool,
            "bytes_col": bytes,
            "datetime_col": datetime.datetime,
        },
    )
    assert ["test_table"] == fresh_db.table_names()
    assert [
        {"name": "text_col", "type": "TEXT"},
        {"name": "float_col", "type": "REAL"},
        {"name": "int_col", "type": "INTEGER"},
        {"name": "bool_col", "type": "INTEGER"},
        {"name": "bytes_col", "type": "BLOB"},
        {"name": "datetime_col", "type": "TEXT"},
    ] == [{"name": col.name, "type": col.type} for col in table.columns]
    assert (
        'CREATE TABLE "test_table" (\n'
        '   "text_col" TEXT,\n'
        '   "float_col" REAL,\n'
        '   "int_col" INTEGER,\n'
        '   "bool_col" INTEGER,\n'
        '   "bytes_col" BLOB,\n'
        '   "datetime_col" TEXT\n'
        ")"
    ) == table.schema


def test_create_table_compound_primary_key(fresh_db):
    table = fresh_db.create_table(
        "test_table", {"id1": str, "id2": str, "value": int}, pk=("id1", "id2")
    )
    assert (
        'CREATE TABLE "test_table" (\n'
        '   "id1" TEXT,\n'
        '   "id2" TEXT,\n'
        '   "value" INTEGER,\n'
        '   PRIMARY KEY ("id1", "id2")\n'
        ")"
    ) == table.schema
    assert ["id1", "id2"] == table.pks


@pytest.mark.parametrize("pk", ("id", ["id"]))
def test_create_table_with_single_primary_key(fresh_db, pk):
    fresh_db.table("foo").insert({"id": 1}, pk=pk)
    assert (
        fresh_db.table("foo").schema
        == 'CREATE TABLE "foo" (\n   "id" INTEGER PRIMARY KEY\n)'
    )


def test_create_table_with_special_column_characters(fresh_db):
    # With double-quote escaping, columns with special characters are now valid
    table = fresh_db.create_table("players", {"name[foo]": str})
    assert ["players"] == fresh_db.table_names()
    assert [{"name": "name[foo]", "type": "TEXT"}] == [
        {"name": col.name, "type": col.type} for col in table.columns
    ]


def test_create_table_with_defaults(fresh_db):
    table = fresh_db.create_table(
        "players",
        {"name": str, "score": int},
        defaults={"score": 1, "name": "bob''bob"},
    )
    assert ["players"] == fresh_db.table_names()
    assert [{"name": "name", "type": "TEXT"}, {"name": "score", "type": "INTEGER"}] == [
        {"name": col.name, "type": col.type} for col in table.columns
    ]
    assert (
        "CREATE TABLE \"players\" (\n   \"name\" TEXT DEFAULT 'bob''''bob',\n   \"score\" INTEGER DEFAULT 1\n)"
    ) == table.schema


def test_create_table_with_bad_not_null(fresh_db):
    with pytest.raises(ValueError):
        fresh_db.create_table(
            "players", {"name": str, "score": int}, not_null={"mouse"}
        )


def test_create_table_with_not_null(fresh_db):
    table = fresh_db.create_table(
        "players",
        {"name": str, "score": int},
        not_null={"name", "score"},
        defaults={"score": 3},
    )
    assert ["players"] == fresh_db.table_names()
    assert [{"name": "name", "type": "TEXT"}, {"name": "score", "type": "INTEGER"}] == [
        {"name": col.name, "type": col.type} for col in table.columns
    ]
    assert (
        'CREATE TABLE "players" (\n   "name" TEXT NOT NULL,\n   "score" INTEGER NOT NULL DEFAULT 3\n)'
    ) == table.schema


@pytest.mark.parametrize(
    "example,expected_columns",
    (
        (
            {"name": "Ravi", "age": 63},
            [{"name": "name", "type": "TEXT"}, {"name": "age", "type": "INTEGER"}],
        ),
        (
            {"create": "Reserved word", "table": "Another"},
            [{"name": "create", "type": "TEXT"}, {"name": "table", "type": "TEXT"}],
        ),
        ({"day": datetime.time(11, 0)}, [{"name": "day", "type": "TEXT"}]),
        ({"decimal": decimal.Decimal("1.2")}, [{"name": "decimal", "type": "REAL"}]),
        (
            {"memoryview": memoryview(b"hello")},
            [{"name": "memoryview", "type": "BLOB"}],
        ),
        ({"uuid": uuid.uuid4()}, [{"name": "uuid", "type": "TEXT"}]),
        ({"foo[bar]": 1}, [{"name": "foo[bar]", "type": "INTEGER"}]),
        (
            {"timedelta": datetime.timedelta(hours=1)},
            [{"name": "timedelta", "type": "TEXT"}],
        ),
    ),
)
def test_create_table_from_example(fresh_db, example, expected_columns):
    people_table = fresh_db.table("people")
    assert people_table.last_rowid is None
    assert people_table.last_pk is None
    people_table.insert(example)
    assert people_table.last_rowid == 1
    assert people_table.last_pk == 1
    assert ["people"] == fresh_db.table_names()
    assert expected_columns == [
        {"name": col.name, "type": col.type} for col in fresh_db.table("people").columns
    ]


def test_create_table_from_example_with_compound_primary_keys(fresh_db):
    record = {"name": "Zhang", "group": "staff", "employee_id": 2}
    table = fresh_db.table("people").insert(record, pk=("group", "employee_id"))
    assert ["group", "employee_id"] == table.pks
    assert record == table.get(("staff", 2))


@pytest.mark.parametrize(
    "method_name", ("insert", "upsert", "insert_all", "upsert_all")
)
@pytest.mark.parametrize("use_old_upsert", (False, True))
def test_create_table_with_custom_columns(method_name, use_old_upsert):
    db = Database(memory=True, use_old_upsert=use_old_upsert)
    table = db.table("dogs")
    method = getattr(table, method_name)
    record = {"id": 1, "name": "Cleo", "age": "5"}
    if method_name.endswith("_all"):
        record = [record]
    method(record, pk="id", columns={"age": int, "weight": float})
    assert ["dogs"] == db.table_names()
    expected_columns = [
        {"name": "id", "type": "INTEGER"},
        {"name": "name", "type": "TEXT"},
        {"name": "age", "type": "INTEGER"},
        {"name": "weight", "type": "REAL"},
    ]
    assert expected_columns == [
        {"name": col.name, "type": col.type} for col in table.columns
    ]
    assert [{"id": 1, "name": "Cleo", "age": 5, "weight": None}] == list(table.rows)


@pytest.mark.parametrize("use_table_factory", [True, False])
def test_create_table_column_order(fresh_db, use_table_factory):
    row = collections.OrderedDict(
        (
            ("zzz", "third"),
            ("abc", "first"),
            ("ccc", "second"),
            ("bbb", "second-to-last"),
            ("aaa", "last"),
        )
    )
    column_order = ("abc", "ccc", "zzz")
    if use_table_factory:
        fresh_db.table("table", column_order=column_order).insert(row)
    else:
        fresh_db.table("table").insert(row, column_order=column_order)
    assert [
        {"name": "abc", "type": "TEXT"},
        {"name": "ccc", "type": "TEXT"},
        {"name": "zzz", "type": "TEXT"},
        {"name": "bbb", "type": "TEXT"},
        {"name": "aaa", "type": "TEXT"},
    ] == [
        {"name": col.name, "type": col.type} for col in fresh_db.table("table").columns
    ]


@pytest.mark.parametrize(
    "foreign_key_specification,expected_exception",
    (
        # You can specify triples, pairs, or a list of columns
        ((("one_id", "one", "id"), ("two_id", "two", "id")), False),
        ((("one_id", "one"), ("two_id", "two")), False),
        (("one_id", "two_id"), False),
        # You can also specify ForeignKey tuples:
        (
            (
                ForeignKey("m2m", "one_id", "one", "id"),
                ForeignKey("m2m", "two_id", "two", "id"),
            ),
            False,
        ),
        # If you specify a column that doesn't point to a table, you  get an error:
        (("one_id", "two_id", "three_id"), NoObviousTable),
        # Tuples of the wrong length get an error:
        ((("one_id", "one", "id", "five"), ("two_id", "two", "id")), ValueError),
        # Likewise a bad column:
        ((("one_id", "one", "id2"),), AlterError),
        # Or a list of dicts
        (({"one_id": "one"},), ValueError),
    ),
)
@pytest.mark.parametrize("use_table_factory", [True, False])
def test_create_table_works_for_m2m_with_only_foreign_keys(
    fresh_db, foreign_key_specification, expected_exception, use_table_factory
):
    if use_table_factory:
        fresh_db.table("one", pk="id").insert({"id": 1})
        fresh_db.table("two", pk="id").insert({"id": 1})
    else:
        fresh_db.table("one").insert({"id": 1}, pk="id")
        fresh_db.table("two").insert({"id": 1}, pk="id")

    row = {"one_id": 1, "two_id": 1}

    def do_it():
        if use_table_factory:
            fresh_db.table("m2m", foreign_keys=foreign_key_specification).insert(row)
        else:
            fresh_db.table("m2m").insert(row, foreign_keys=foreign_key_specification)

    if expected_exception:
        with pytest.raises(expected_exception):
            do_it()
        return
    else:
        do_it()
    assert [
        {"name": "one_id", "type": "INTEGER"},
        {"name": "two_id", "type": "INTEGER"},
    ] == [{"name": col.name, "type": col.type} for col in fresh_db.table("m2m").columns]
    assert sorted(
        [
            {"column": "one_id", "other_table": "one", "other_column": "id"},
            {"column": "two_id", "other_table": "two", "other_column": "id"},
        ],
        key=lambda s: repr(s),
    ) == sorted(
        [
            {
                "column": fk.column,
                "other_table": fk.other_table,
                "other_column": fk.other_column,
            }
            for fk in fresh_db.table("m2m").foreign_keys
        ],
        key=lambda s: repr(s),
    )


def test_self_referential_foreign_key(fresh_db):
    assert [] == fresh_db.table_names()
    table = fresh_db.create_table(
        "test_table",
        columns={
            "id": int,
            "ref": int,
        },
        pk="id",
        foreign_keys=(("ref", "test_table", "id"),),
    )
    assert (
        'CREATE TABLE "test_table" (\n'
        '   "id" INTEGER PRIMARY KEY,\n'
        '   "ref" INTEGER REFERENCES "test_table"("id")\n'
        ")"
    ) == table.schema


def test_create_error_if_invalid_foreign_keys(fresh_db):
    with pytest.raises(AlterError):
        fresh_db.table("one").insert(
            {"id": 1, "ref_id": 3},
            pk="id",
            foreign_keys=(("ref_id", "bad_table", "bad_column"),),
        )


def test_create_error_if_invalid_self_referential_foreign_keys(fresh_db):
    with pytest.raises(AlterError) as ex:
        fresh_db.table("one").insert(
            {"id": 1, "ref_id": 3},
            pk="id",
            foreign_keys=(("ref_id", "one", "bad_column"),),
        )
        assert ex.value.args == ("No such column: one.bad_column",)


@pytest.mark.parametrize(
    "col_name,col_type,not_null_default,expected_schema",
    (
        (
            "nickname",
            str,
            None,
            'CREATE TABLE "dogs" (\n   "name" TEXT\n, "nickname" TEXT)',
        ),
        (
            "dob",
            datetime.date,
            None,
            'CREATE TABLE "dogs" (\n   "name" TEXT\n, "dob" TEXT)',
        ),
        ("age", int, None, 'CREATE TABLE "dogs" (\n   "name" TEXT\n, "age" INTEGER)'),
        (
            "weight",
            float,
            None,
            'CREATE TABLE "dogs" (\n   "name" TEXT\n, "weight" REAL)',
        ),
        ("text", "TEXT", None, 'CREATE TABLE "dogs" (\n   "name" TEXT\n, "text" TEXT)'),
        (
            "integer",
            "INTEGER",
            None,
            'CREATE TABLE "dogs" (\n   "name" TEXT\n, "integer" INTEGER)',
        ),
        (
            "float",
            "FLOAT",
            None,
            'CREATE TABLE "dogs" (\n   "name" TEXT\n, "float" FLOAT)',
        ),
        ("blob", "blob", None, 'CREATE TABLE "dogs" (\n   "name" TEXT\n, "blob" BLOB)'),
        (
            "default_str",
            None,
            None,
            'CREATE TABLE "dogs" (\n   "name" TEXT\n, "default_str" TEXT)',
        ),
        (
            "nickname",
            str,
            "",
            'CREATE TABLE "dogs" (\n   "name" TEXT\n, "nickname" TEXT NOT NULL DEFAULT \'\')',
        ),
        (
            "nickname",
            str,
            "dawg's dawg",
            'CREATE TABLE "dogs" (\n   "name" TEXT\n, "nickname" TEXT NOT NULL DEFAULT \'dawg\'\'s dawg\')',
        ),
    ),
)
def test_add_column(fresh_db, col_name, col_type, not_null_default, expected_schema):
    fresh_db.create_table("dogs", {"name": str})
    assert fresh_db.table("dogs").schema == 'CREATE TABLE "dogs" (\n   "name" TEXT\n)'
    fresh_db.table("dogs").add_column(
        col_name, col_type, not_null_default=not_null_default
    )
    assert fresh_db.table("dogs").schema == expected_schema


def test_add_foreign_key(fresh_db):
    fresh_db.table("authors").insert_all(
        [{"id": 1, "name": "Sally"}, {"id": 2, "name": "Asheesh"}], pk="id"
    )
    fresh_db.table("books").insert_all(
        [
            {"title": "Hedgehogs of the world", "author_id": 1},
            {"title": "How to train your wolf", "author_id": 2},
        ]
    )
    assert [] == fresh_db.table("books").foreign_keys
    t = fresh_db.table("books").add_foreign_key("author_id", "authors", "id")
    # Ensure it returned self:
    assert isinstance(t, Table) and t.name == "books"
    assert [
        ForeignKey(
            table="books", column="author_id", other_table="authors", other_column="id"
        )
    ] == fresh_db.table("books").foreign_keys


def test_add_foreign_key_if_column_contains_space(fresh_db):
    fresh_db.table("authors").insert_all([{"id": 1, "name": "Sally"}], pk="id")
    fresh_db.table("books").insert_all(
        [
            {"title": "Hedgehogs of the world", "author id": 1},
        ]
    )
    fresh_db.table("books").add_foreign_key("author id", "authors", "id")
    assert fresh_db.table("books").foreign_keys == [
        ForeignKey(
            table="books", column="author id", other_table="authors", other_column="id"
        )
    ]


def test_add_foreign_key_error_if_column_does_not_exist(fresh_db):
    fresh_db.table("books").insert(
        {"id": 1, "title": "Hedgehogs of the world", "author_id": 1}
    )
    with pytest.raises(AlterError):
        fresh_db.table("books").add_foreign_key("author2_id", "books", "id")


def test_add_foreign_key_error_if_other_table_does_not_exist(fresh_db):
    fresh_db.table("books").insert({"title": "Hedgehogs of the world", "author_id": 1})
    with pytest.raises(AlterError):
        fresh_db.table("books").add_foreign_key("author_id", "authors", "id")


def test_add_foreign_key_error_if_already_exists(fresh_db):
    fresh_db.table("books").insert({"title": "Hedgehogs of the world", "author_id": 1})
    fresh_db.table("authors").insert({"id": 1, "name": "Sally"}, pk="id")
    fresh_db.table("books").add_foreign_key("author_id", "authors", "id")
    with pytest.raises(AlterError) as ex:
        fresh_db.table("books").add_foreign_key("author_id", "authors", "id")
    assert "Foreign key already exists for author_id => authors.id" == ex.value.args[0]


def test_add_foreign_key_no_error_if_exists_and_ignore_true(fresh_db):
    fresh_db.table("books").insert({"title": "Hedgehogs of the world", "author_id": 1})
    fresh_db.table("authors").insert({"id": 1, "name": "Sally"}, pk="id")
    fresh_db.table("books").add_foreign_key("author_id", "authors", "id")
    fresh_db.table("books").add_foreign_key("author_id", "authors", "id", ignore=True)


def test_add_foreign_keys(fresh_db):
    fresh_db.table("authors").insert_all(
        [{"id": 1, "name": "Sally"}, {"id": 2, "name": "Asheesh"}], pk="id"
    )
    fresh_db.table("categories").insert_all([{"id": 1, "name": "Wildlife"}], pk="id")
    fresh_db.table("books").insert_all(
        [{"title": "Hedgehogs of the world", "author_id": 1, "category_id": 1}]
    )
    assert [] == fresh_db.table("books").foreign_keys
    fresh_db.add_foreign_keys(
        [
            ("books", "author_id", "authors", "id"),
            ("books", "category_id", "categories", "id"),
        ]
    )
    assert [
        ForeignKey(
            table="books", column="author_id", other_table="authors", other_column="id"
        ),
        ForeignKey(
            table="books",
            column="category_id",
            other_table="categories",
            other_column="id",
        ),
    ] == sorted(fresh_db.table("books").foreign_keys)


def test_add_column_foreign_key(fresh_db):
    fresh_db.create_table("dogs", {"name": str})
    fresh_db.create_table("breeds", {"name": str})
    fresh_db.table("dogs").add_column("breed_id", fk="breeds")
    assert fresh_db.table("dogs").schema == (
        'CREATE TABLE "dogs" (\n'
        '   "name" TEXT,\n'
        '   "breed_id" INTEGER REFERENCES "breeds"("rowid")\n'
        ")"
    )
    # And again with an explicit primary key column
    fresh_db.create_table("subbreeds", {"name": str, "primkey": str}, pk="primkey")
    fresh_db.table("dogs").add_column("subbreed_id", fk="subbreeds")
    assert fresh_db.table("dogs").schema == (
        'CREATE TABLE "dogs" (\n'
        '   "name" TEXT,\n'
        '   "breed_id" INTEGER REFERENCES "breeds"("rowid"),\n'
        '   "subbreed_id" TEXT REFERENCES "subbreeds"("primkey")\n'
        ")"
    )


def test_add_foreign_key_guess_table(fresh_db):
    fresh_db.create_table("dogs", {"name": str})
    fresh_db.create_table("breeds", {"name": str, "id": int}, pk="id")
    fresh_db.table("dogs").add_column("breed_id", int)
    fresh_db.table("dogs").add_foreign_key("breed_id")
    assert fresh_db.table("dogs").schema == (
        'CREATE TABLE "dogs" (\n'
        '   "name" TEXT,\n'
        '   "breed_id" INTEGER REFERENCES "breeds"("id")\n'
        ")"
    )


def test_index_foreign_keys(fresh_db):
    test_add_foreign_key_guess_table(fresh_db)
    assert [] == fresh_db.table("dogs").indexes
    fresh_db.index_foreign_keys()
    assert [["breed_id"]] == [i.columns for i in fresh_db.table("dogs").indexes]
    # Calling it a second time should do nothing
    fresh_db.index_foreign_keys()
    assert [["breed_id"]] == [i.columns for i in fresh_db.table("dogs").indexes]


def test_index_foreign_keys_if_index_name_is_already_used(fresh_db):
    # https://github.com/simonw/sqlite-utils/issues/335
    test_add_foreign_key_guess_table(fresh_db)
    # Add index with a name that will conflict with index_foreign_keys()
    fresh_db.table("dogs").create_index(["name"], index_name="idx_dogs_breed_id")
    fresh_db.index_foreign_keys()
    assert {
        (idx.name, tuple(idx.columns)) for idx in fresh_db.table("dogs").indexes
    } == {
        ("idx_dogs_breed_id_2", ("breed_id",)),
        ("idx_dogs_breed_id", ("name",)),
    }


@pytest.mark.parametrize(
    "extra_data,expected_new_columns",
    [
        ({"species": "squirrels"}, [{"name": "species", "type": "TEXT"}]),
        (
            {"species": "squirrels", "hats": 5},
            [{"name": "species", "type": "TEXT"}, {"name": "hats", "type": "INTEGER"}],
        ),
        (
            {"hats": 5, "rating": 3.5},
            [{"name": "hats", "type": "INTEGER"}, {"name": "rating", "type": "REAL"}],
        ),
    ],
)
@pytest.mark.parametrize("use_table_factory", [True, False])
def test_insert_row_alter_table(
    fresh_db, extra_data, expected_new_columns, use_table_factory
):
    table = fresh_db.table("books")
    table.insert({"title": "Hedgehogs of the world", "author_id": 1})
    assert [
        {"name": "title", "type": "TEXT"},
        {"name": "author_id", "type": "INTEGER"},
    ] == [{"name": col.name, "type": col.type} for col in table.columns]
    record = {"title": "Squirrels of the world", "author_id": 2}
    record.update(extra_data)
    if use_table_factory:
        fresh_db.table("books", alter=True).insert(record)
    else:
        fresh_db.table("books").insert(record, alter=True)
    assert [
        {"name": "title", "type": "TEXT"},
        {"name": "author_id", "type": "INTEGER"},
    ] + expected_new_columns == [
        {"name": col.name, "type": col.type} for col in table.columns
    ]


def test_add_missing_columns_case_insensitive(fresh_db):
    table = fresh_db.table("foo")
    table.insert({"id": 1, "name": "Cleo"}, pk="id")
    table.add_missing_columns([{"Name": ".", "age": 4}])
    assert (
        table.schema
        == 'CREATE TABLE "foo" (\n   "id" INTEGER PRIMARY KEY,\n   "name" TEXT\n, "age" INTEGER)'
    )


@pytest.mark.parametrize("use_table_factory", [True, False])
def test_insert_replace_rows_alter_table(fresh_db, use_table_factory):
    first_row = {"id": 1, "title": "Hedgehogs of the world", "author_id": 1}
    next_rows = [
        {"id": 1, "title": "Hedgehogs of the World", "species": "hedgehogs"},
        {"id": 2, "title": "Squirrels of the World", "num_species": 200},
        {
            "id": 3,
            "title": "Badgers of the World",
            "significant_continents": ["Europe", "North America"],
        },
    ]
    if use_table_factory:
        table = fresh_db.table("books", pk="id", alter=True)
        table.insert(first_row)
        table.insert_all(next_rows, replace=True)
    else:
        table = fresh_db.table("books")
        table.insert(first_row, pk="id")
        table.insert_all(next_rows, alter=True, replace=True)
    assert {
        "author_id": int,
        "id": int,
        "num_species": int,
        "significant_continents": str,
        "species": str,
        "title": str,
    } == table.columns_dict
    assert [
        {
            "author_id": None,
            "id": 1,
            "num_species": None,
            "significant_continents": None,
            "species": "hedgehogs",
            "title": "Hedgehogs of the World",
        },
        {
            "author_id": None,
            "id": 2,
            "num_species": 200,
            "significant_continents": None,
            "species": None,
            "title": "Squirrels of the World",
        },
        {
            "author_id": None,
            "id": 3,
            "num_species": None,
            "significant_continents": '["Europe", "North America"]',
            "species": None,
            "title": "Badgers of the World",
        },
    ] == list(table.rows)


def test_insert_all_with_extra_columns_in_later_chunks(fresh_db):
    chunk = [
        {"record": "Record 1"},
        {"record": "Record 2"},
        {"record": "Record 3"},
        {"record": "Record 4", "extra": 1},
    ]
    fresh_db.table("t").insert_all(chunk, batch_size=2, alter=True)
    assert list(fresh_db.table("t").rows) == [
        {"record": "Record 1", "extra": None},
        {"record": "Record 2", "extra": None},
        {"record": "Record 3", "extra": None},
        {"record": "Record 4", "extra": 1},
    ]


def test_bulk_insert_more_than_999_values(fresh_db):
    "Inserting 100 items with 11 columns should work"
    fresh_db.table("big").insert_all(
        (
            {
                "id": i + 1,
                "c2": 2,
                "c3": 3,
                "c4": 4,
                "c5": 5,
                "c6": 6,
                "c7": 7,
                "c8": 8,
                "c9": 9,
                "c10": 10,
                "c11": 11,
            }
            for i in range(100)
        ),
        pk="id",
    )
    assert fresh_db.table("big").count == 100


@pytest.mark.parametrize(
    "num_columns,should_error", ((900, False), (999, False), (1000, True))
)
def test_error_if_more_than_999_columns(fresh_db, num_columns, should_error):
    record = {f"c{i}": i for i in range(num_columns)}
    if should_error:
        with pytest.raises(ValueError):
            fresh_db.table("big").insert(record)
    else:
        fresh_db.table("big").insert(record)


def test_columns_not_in_first_record_should_not_cause_batch_to_be_too_large(fresh_db):
    # https://github.com/simonw/sqlite-utils/issues/145
    # sqlite on homebrew and Debian/Ubuntu etc. is typically compiled with
    #  SQLITE_MAX_VARIABLE_NUMBER set to 250,000, so we need to exceed this value to
    #  trigger the error on these systems.
    THRESHOLD = 250000
    batch_size = 999
    extra_columns = 1 + (THRESHOLD - 1) // (batch_size - 1)
    records = [
        {"c0": "first record"},  # one column in first record -> batch size = 999
        # fill out the batch with 99 records with enough columns to exceed THRESHOLD
        *[{f"c{i}": j for i in range(extra_columns)} for j in range(batch_size - 1)],
    ]
    fresh_db.table("too_many_columns").insert_all(
        records, alter=True, batch_size=batch_size
    )


@pytest.mark.parametrize(
    "columns,index_name,expected_index",
    (
        (
            ["is good dog"],
            None,
            Index(
                seq=0,
                name="idx_dogs_is good dog",
                unique=0,
                origin="c",
                partial=0,
                columns=["is good dog"],
            ),
        ),
        (
            ["is good dog", "age"],
            None,
            Index(
                seq=0,
                name="idx_dogs_is good dog_age",
                unique=0,
                origin="c",
                partial=0,
                columns=["is good dog", "age"],
            ),
        ),
        (
            ["age"],
            "age_index",
            Index(
                seq=0,
                name="age_index",
                unique=0,
                origin="c",
                partial=0,
                columns=["age"],
            ),
        ),
    ),
)
def test_create_index(fresh_db, columns, index_name, expected_index):
    dogs = fresh_db.table("dogs")
    dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is good dog": True})
    assert [] == dogs.indexes
    dogs.create_index(columns, index_name)
    assert expected_index == dogs.indexes[0]


def test_create_index_unique(fresh_db):
    dogs = fresh_db.table("dogs")
    dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is_good_dog": True})
    assert [] == dogs.indexes
    dogs.create_index(["name"], unique=True)
    assert (
        Index(
            seq=0,
            name="idx_dogs_name",
            unique=1,
            origin="c",
            partial=0,
            columns=["name"],
        )
        == dogs.indexes[0]
    )


def test_create_index_if_not_exists(fresh_db):
    dogs = fresh_db.table("dogs")
    dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is_good_dog": True})
    assert [] == dogs.indexes
    dogs.create_index(["name"])
    assert len(dogs.indexes) == 1
    with pytest.raises(Exception, match="index idx_dogs_name already exists"):
        dogs.create_index(["name"])
    dogs.create_index(["name"], if_not_exists=True)


def test_drop_index(fresh_db):
    dogs = fresh_db.table("dogs")
    dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is_good_dog": True})
    dogs.create_index(["name"])
    assert [index.name for index in dogs.indexes] == ["idx_dogs_name"]
    dogs.drop_index("idx_dogs_name")
    assert dogs.indexes == []


def test_drop_index_ignore(fresh_db):
    dogs = fresh_db.table("dogs")
    dogs.insert({"name": "Cleo"})
    with pytest.raises(OperationalError, match="No index named idx_dogs_name"):
        dogs.drop_index("idx_dogs_name")
    dogs.drop_index("idx_dogs_name", ignore=True)


def test_drop_index_wrong_table(fresh_db):
    dogs = fresh_db.table("dogs")
    cats = fresh_db.table("cats")
    dogs.insert({"name": "Cleo"})
    cats.insert({"name": "Misty"})
    dogs.create_index(["name"])
    with pytest.raises(OperationalError, match="No index named idx_dogs_name"):
        cats.drop_index("idx_dogs_name")
    assert [index.name for index in dogs.indexes] == ["idx_dogs_name"]


def test_create_index_desc(fresh_db):
    dogs = fresh_db.table("dogs")
    dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is good dog": True})
    assert [] == dogs.indexes
    dogs.create_index([DescIndex("age"), "name"])
    sql = fresh_db.execute(
        "select sql from sqlite_master where name='idx_dogs_age_name'"
    ).fetchone()[0]
    assert sql == (
        'CREATE INDEX "idx_dogs_age_name"\n' '    ON "dogs" ("age" desc, "name")'
    )


def test_create_index_find_unique_name(fresh_db):
    table = fresh_db.table("t")
    table.insert({"id": 1})
    table.create_index(["id"])
    # Without find_unique_name should error
    with pytest.raises(OperationalError, match="index idx_t_id already exists"):
        table.create_index(["id"])
    # With find_unique_name=True it should work
    table.create_index(["id"], find_unique_name=True)
    table.create_index(["id"], find_unique_name=True)
    # Should have three now
    index_names = {idx.name for idx in table.indexes}
    assert index_names == {"idx_t_id", "idx_t_id_2", "idx_t_id_3"}


def test_create_index_analyze(fresh_db):
    dogs = fresh_db.table("dogs")
    assert "sqlite_stat1" not in fresh_db.table_names()
    dogs.insert({"name": "Cleo", "twitter": "cleopaws"})
    dogs.create_index(["name"], analyze=True)
    assert "sqlite_stat1" in fresh_db.table_names()
    assert list(fresh_db.table("sqlite_stat1").rows) == [
        {"tbl": "dogs", "idx": "idx_dogs_name", "stat": "1 1"}
    ]


@pytest.mark.parametrize(
    "data_structure",
    (
        ["list with one item"],
        ["list with", "two items"],
        {"dictionary": "simple"},
        {"dictionary": {"nested": "complex"}},
        collections.OrderedDict(
            [
                ("key1", {"nested": ["cømplex"]}),
                ("key2", "foo"),
            ]
        ),
        [{"list": "of"}, {"two": "dicts"}],
    ),
)
def test_insert_dictionaries_and_lists_as_json(fresh_db, data_structure):
    fresh_db.table("test").insert({"id": 1, "data": data_structure}, pk="id")
    row = fresh_db.execute("select id, data from test").fetchone()
    assert row[0] == 1
    assert data_structure == json.loads(row[1])


def test_insert_list_nested_unicode(fresh_db):
    fresh_db.table("test").insert(
        {"id": 1, "data": {"key1": {"nested": ["cømplex"]}}}, pk="id"
    )
    row = fresh_db.execute("select id, data from test").fetchone()
    assert row[1] == '{"key1": {"nested": ["cømplex"]}}'


def test_insert_uuid(fresh_db):
    uuid4 = uuid.uuid4()
    fresh_db.table("test").insert({"uuid": uuid4})
    row = next(iter(fresh_db.table("test").rows))
    assert {"uuid"} == row.keys()
    assert isinstance(row["uuid"], str)
    assert row["uuid"] == str(uuid4)


def test_insert_memoryview(fresh_db):
    fresh_db.table("test").insert({"data": memoryview(b"hello")})
    row = next(iter(fresh_db.table("test").rows))
    assert {"data"} == row.keys()
    assert isinstance(row["data"], bytes)
    assert row["data"] == b"hello"


def test_insert_thousands_using_generator(fresh_db):
    fresh_db.table("test").insert_all(
        {"i": i, "word": f"word_{i}"} for i in range(10000)
    )
    assert [{"name": "i", "type": "INTEGER"}, {"name": "word", "type": "TEXT"}] == [
        {"name": col.name, "type": col.type} for col in fresh_db.table("test").columns
    ]
    assert fresh_db.table("test").count == 10000


def test_insert_thousands_raises_exception_with_extra_columns_after_first_100(fresh_db):
    # https://github.com/simonw/sqlite-utils/issues/139
    with pytest.raises(Exception, match="table test has no column named extra"):
        fresh_db.table("test").insert_all(
            [{"i": i, "word": f"word_{i}"} for i in range(100)]
            + [{"i": 101, "extra": "This extra column should cause an exception"}],
        )


def test_insert_thousands_adds_extra_columns_after_first_100_with_alter(fresh_db):
    # https://github.com/simonw/sqlite-utils/issues/139
    fresh_db.table("test").insert_all(
        [{"i": i, "word": f"word_{i}"} for i in range(100)]
        + [{"i": 101, "extra": "Should trigger ALTER"}],
        alter=True,
    )
    rows = list(fresh_db.query("select * from test where i = 101"))
    assert rows == [{"i": 101, "word": None, "extra": "Should trigger ALTER"}]


@pytest.mark.parametrize("num_rows", (0, 1, 2, 3, 10))
def test_insert_all_pk_not_in_records_raises(fresh_db, num_rows):
    # https://github.com/simonw/sqlite-utils/issues/732
    fresh_db.conn.execute("CREATE TABLE t (a TEXT, b INT, PRIMARY KEY (a, b))")
    rows = [{"a": f"x{i}", "b": i} for i in range(num_rows)]

    with pytest.raises(InvalidColumns) as ex:
        fresh_db.table("t").insert_all(rows, pk="not_a_column")

    assert ex.value.args == (
        "Invalid primary key column ['not_a_column'] for table t with columns ['a', 'b']",
    )
    assert fresh_db.table("t").count == 0


@pytest.mark.parametrize("num_rows", (1, 2, 3, 10))
def test_insert_all_pk_not_in_records_alter_raises(fresh_db, num_rows):
    # With alter=True the check is deferred until the record keys are
    # known - a pk column that is in neither the table nor the records
    # still raises
    fresh_db.conn.execute("CREATE TABLE t (a TEXT, b INT, PRIMARY KEY (a, b))")
    rows = [{"a": f"x{i}", "b": i} for i in range(num_rows)]

    with pytest.raises(InvalidColumns) as ex:
        fresh_db.table("t").insert_all(rows, pk="not_a_column", alter=True)

    assert ex.value.args == (
        "Invalid primary key column ['not_a_column'] for table t with columns ['a', 'b']",
    )
    assert fresh_db.table("t").count == 0


def test_insert_pk_in_records_with_alter_adds_column(fresh_db):
    # 3.x allowed insert(pk=..., alter=True) to add the pk column from the
    # records - the InvalidColumns check must not fire in that case
    fresh_db.table("t").insert({"a": 1})
    fresh_db.table("t").insert({"id": 5, "a": 2}, pk="id", alter=True)
    assert fresh_db.table("t").columns_dict.keys() == {"a", "id"}
    assert list(fresh_db.query("select * from t order by a")) == [
        {"a": 1, "id": None},
        {"a": 2, "id": 5},
    ]


def test_insert_all_invalid_pk_alter_empty_records_is_noop(fresh_db):
    # With alter=True the pk check needs record keys, so an empty iterator
    # returns without error - matching the 3.x no-op for empty inserts
    fresh_db.conn.execute("CREATE TABLE t (a TEXT)")
    fresh_db.table("t").insert_all([], pk="not_a_column", alter=True)
    assert fresh_db.table("t").count == 0


def test_insert_ignore(fresh_db):
    fresh_db.table("test").insert({"id": 1, "bar": 2}, pk="id")
    # Should raise an error if we try this again
    with pytest.raises(Exception, match="UNIQUE constraint failed"):
        fresh_db.table("test").insert({"id": 1, "bar": 2}, pk="id")
    # Using ignore=True should cause our insert to be silently ignored
    fresh_db.table("test").insert({"id": 1, "bar": 3}, pk="id", ignore=True)
    # Only one row, and it should be bar=2, not bar=3
    rows = list(fresh_db.query("select * from test"))
    assert rows == [{"id": 1, "bar": 2}]


def test_insert_ignore_reports_existing_row(fresh_db):
    # An ignored insert (row already exists) should point last_rowid and
    # last_pk at the existing conflicting row - see the Datasette insert API
    fresh_db.table("docs").insert({"id": 1, "title": "Exists"}, pk="id")
    # Insert a conflicting row with ignore=True and no explicit pk=
    table = fresh_db.table("docs").insert({"id": 1, "title": "One"}, ignore=True)
    assert table.last_rowid == 1
    assert table.last_pk == 1
    assert list(fresh_db.table("docs").rows_where("rowid = ?", [table.last_rowid])) == [
        {"id": 1, "title": "Exists"}
    ]


@pytest.mark.parametrize("rowid_alias", ("rowid", "_rowid_", "oid"))
@pytest.mark.parametrize("method", ("upsert", "insert_replace", "insert_ignore"))
def test_pk_rowid_alias_on_rowid_table(fresh_db, rowid_alias, method):
    # rowid and its aliases are valid primary keys for a rowid table even
    # though they are not listed among the table's columns - see the Datasette
    # upsert API against tables without an explicit primary key
    fresh_db.table("t").insert({"title": "Hello"})
    assert fresh_db.table("t").pks == ["rowid"]
    record = {rowid_alias: 1, "title": "Updated"}
    if method == "upsert":
        table = fresh_db.table("t").upsert(record, pk=rowid_alias)
    elif method == "insert_replace":
        table = fresh_db.table("t").insert(record, pk=rowid_alias, replace=True)
    else:
        table = fresh_db.table("t").insert(record, pk=rowid_alias, ignore=True)
    assert table.last_pk == 1
    expected_title = "Hello" if method == "insert_ignore" else "Updated"
    assert list(fresh_db.table("t").rows) == [{"title": expected_title}]


def test_insert_ignore_reports_existing_row_compound_pk(fresh_db):
    # Compound primary key variant of the ignored-insert lookup
    fresh_db.table("t").insert_all([{"a": 1, "b": 2, "note": "first"}], pk=("a", "b"))
    table = fresh_db.table("t").insert(
        {"a": 1, "b": 2, "note": "second"}, pk=("a", "b"), ignore=True
    )
    assert table.last_pk == (1, 2)
    assert list(fresh_db.table("t").rows_where("rowid = ?", [table.last_rowid])) == [
        {"a": 1, "b": 2, "note": "first"}
    ]


def test_insert_ignore_reports_existing_row_list_mode(fresh_db):
    # List-based iteration variant of the ignored-insert lookup
    fresh_db.table("t").insert_all([["id", "title"], [1, "first"]], pk="id")
    table = fresh_db.table("t").insert_all(
        [["id", "title"], [1, "second"]], pk="id", ignore=True
    )
    assert table.last_pk == 1
    assert table.last_rowid == 1
    assert list(fresh_db.table("t").rows) == [{"id": 1, "title": "first"}]


def test_insert_ignore_hash_id_reports_pk(fresh_db):
    # With hash_id the pk is the computed hash; the original record has no id
    # column to look up so last_rowid is left unset
    first = fresh_db.table("dogs").insert({"name": "Cleo"}, hash_id="id")
    table = fresh_db.table("dogs").insert({"name": "Cleo"}, hash_id="id", ignore=True)
    assert table.last_pk == first.last_pk
    assert table.last_rowid is None
    assert fresh_db.table("dogs").count == 1


def test_insert_ignore_unresolvable_conflict_leaves_pk_unset(fresh_db):
    # When the conflict cannot be resolved to a primary key lookup, last_pk and
    # last_rowid are left unset rather than reporting a misleading value

    # rowid table with a UNIQUE column and no primary key: no pk to look up
    fresh_db.table("u").db.execute("create table u (title text unique)")
    fresh_db.table("u").insert({"title": "x"})
    table = fresh_db.table("u").insert({"title": "x"}, ignore=True)
    assert table.last_pk is None
    assert table.last_rowid is None
    assert fresh_db.table("u").count == 1

    # Conflict on a UNIQUE column other than the primary key: the pk value from
    # the record does not match the existing row, so the lookup finds nothing
    fresh_db.table("docs").db.execute(
        "create table docs (id integer primary key, email text unique)"
    )
    fresh_db.table("docs").insert({"id": 1, "email": "a"}, pk="id")
    table = fresh_db.table("docs").insert({"id": 2, "email": "a"}, ignore=True)
    assert table.last_pk is None
    assert table.last_rowid is None
    assert fresh_db.table("docs").count == 1


def test_insert_ignore_with_pk_after_other_table_insert(fresh_db):
    # https://github.com/simonw/sqlite-utils/issues/554
    user = {"id": "abc", "name": "david"}

    fresh_db.table("users").insert(user, pk="id")
    fresh_db.table("comments").insert_all(
        [
            {"id": "def", "text": "ok"},
            {"id": "ghi", "text": "great"},
        ],
    )

    table = fresh_db.table("users").insert(user, pk="id", ignore=True)

    assert table.last_pk == "abc"
    assert list(fresh_db.table("users").rows) == [user]


def test_insert_hash_id(fresh_db):
    dogs = fresh_db.table("dogs")
    id = dogs.insert({"name": "Cleo", "twitter": "cleopaws"}, hash_id="id").last_pk
    assert "f501265970505d9825d8d9f590bfab3519fb20b1" == id
    assert dogs.count == 1
    # Insert replacing a second time should not create a new row
    id2 = dogs.insert(
        {"name": "Cleo", "twitter": "cleopaws"}, hash_id="id", replace=True
    ).last_pk
    assert "f501265970505d9825d8d9f590bfab3519fb20b1" == id2
    assert dogs.count == 1


@pytest.mark.parametrize("use_table_factory", [True, False])
def test_insert_hash_id_columns(fresh_db, use_table_factory):
    if use_table_factory:
        dogs = fresh_db.table("dogs", hash_id_columns=("name", "twitter"))
        insert_kwargs = {}
    else:
        dogs = fresh_db.table("dogs")
        insert_kwargs = {"hash_id_columns": ("name", "twitter")}

    id = dogs.insert(
        {"name": "Cleo", "twitter": "cleopaws", "age": 5},
        **insert_kwargs,
    ).last_pk
    expected_hash = hash_record({"name": "Cleo", "twitter": "cleopaws"})
    assert id == expected_hash
    assert dogs.count == 1
    # Insert replacing a second time should not create a new row
    id2 = dogs.insert(
        {"name": "Cleo", "twitter": "cleopaws", "age": 6},
        **insert_kwargs,
        replace=True,
    ).last_pk
    assert id2 == expected_hash
    assert dogs.count == 1


def test_vacuum(fresh_db):
    fresh_db.table("data").insert({"foo": "foo", "bar": "bar"})
    fresh_db.vacuum()


def test_works_with_pathlib_path(tmpdir):
    path = pathlib.Path(tmpdir / "test.db")
    db = Database(path)
    db.table("demo").insert_all([{"foo": 1}])
    assert db.table("demo").count == 1


@pytest.mark.skipif(pd is None, reason="pandas and numpy are not installed")
def test_create_table_numpy(fresh_db):
    assert pd is not None
    df = pd.DataFrame({"col 1": range(3), "col 2": range(3)})
    fresh_db.table("pandas").insert_all(df.to_dict(orient="records"))
    assert [
        {"col 1": 0, "col 2": 0},
        {"col 1": 1, "col 2": 1},
        {"col 1": 2, "col 2": 2},
    ] == list(fresh_db.table("pandas").rows)
    # Now try all the different types
    df = pd.DataFrame(
        {
            "np.int8": [-8],
            "np.int16": [-16],
            "np.int32": [-32],
            "np.int64": [-64],
            "np.uint8": [8],
            "np.uint16": [16],
            "np.uint32": [32],
            "np.uint64": [64],
            "np.float16": [16.5],
            "np.float32": [32.5],
            "np.float64": [64.5],
        }
    )
    df = df.astype(
        {
            "np.int8": "int8",
            "np.int16": "int16",
            "np.int32": "int32",
            "np.int64": "int64",
            "np.uint8": "uint8",
            "np.uint16": "uint16",
            "np.uint32": "uint32",
            "np.uint64": "uint64",
            "np.float16": "float16",
            "np.float32": "float32",
            "np.float64": "float64",
        }
    )
    assert [
        "int8",
        "int16",
        "int32",
        "int64",
        "uint8",
        "uint16",
        "uint32",
        "uint64",
        "float16",
        "float32",
        "float64",
    ] == [str(t) for t in df.dtypes]
    fresh_db.table("types").insert_all(df.to_dict(orient="records"))
    assert [
        {
            "np.float16": 16.5,
            "np.float32": 32.5,
            "np.float64": 64.5,
            "np.int16": -16,
            "np.int32": -32,
            "np.int64": -64,
            "np.int8": -8,
            "np.uint16": 16,
            "np.uint32": 32,
            "np.uint64": 64,
            "np.uint8": 8,
        }
    ] == list(fresh_db.table("types").rows)


def test_cannot_provide_both_filename_and_memory():
    with pytest.raises(
        ValueError, match="Either specify a filename_or_conn or pass memory=True"
    ):
        Database("/tmp/foo.db", memory=True)


def test_creates_id_column(fresh_db):
    last_pk = fresh_db.table("cats", pk="id").insert({"name": "barry"}).last_pk
    assert [{"name": "barry", "id": last_pk}] == list(fresh_db.table("cats").rows)


def test_drop(fresh_db):
    fresh_db.table("t").insert({"foo": 1})
    assert ["t"] == fresh_db.table_names()
    assert None is fresh_db.table("t").drop()
    assert [] == fresh_db.table_names()


def test_drop_view(fresh_db):
    fresh_db.create_view("foo_view", "select 1")
    assert ["foo_view"] == fresh_db.view_names()
    assert None is fresh_db.view("foo_view").drop()
    assert [] == fresh_db.view_names()


def test_drop_ignore(fresh_db):
    with pytest.raises(sqlite3.OperationalError):
        fresh_db.table("does_not_exist").drop()
    fresh_db.table("does_not_exist").drop(ignore=True)
    # Testing view is harder, we need to create it in order
    # to get a View object, then drop it twice
    fresh_db.create_view("foo_view", "select 1")
    view = fresh_db.view("foo_view")
    assert isinstance(view, View)
    view.drop()
    with pytest.raises(sqlite3.OperationalError):
        view.drop()
    view.drop(ignore=True)


def test_insert_all_empty_list(fresh_db):
    fresh_db.table("t").insert({"foo": 1})
    assert fresh_db.table("t").count == 1
    fresh_db.table("t").insert_all([])
    assert fresh_db.table("t").count == 1
    fresh_db.table("t").insert_all([], replace=True)
    assert fresh_db.table("t").count == 1


def test_insert_all_single_column(fresh_db):
    table = fresh_db.table("table")
    table.insert_all([{"name": "Cleo"}], pk="name")
    assert [{"name": "Cleo"}] == list(table.rows)
    assert table.pks == ["name"]


@pytest.mark.parametrize("method_name", ("insert_all", "upsert_all"))
def test_insert_all_analyze(fresh_db, method_name):
    table = fresh_db.table("table")
    table.insert_all([{"id": 1, "name": "Cleo"}], pk="id")
    assert "sqlite_stat1" not in fresh_db.table_names()
    table.create_index(["name"], analyze=True)
    assert list(fresh_db.table("sqlite_stat1").rows) == [
        {"tbl": "table", "idx": "idx_table_name", "stat": "1 1"}
    ]
    method = getattr(table, method_name)
    method([{"id": 2, "name": "Suna"}], pk="id", analyze=True)
    assert "sqlite_stat1" in fresh_db.table_names()
    assert list(fresh_db.table("sqlite_stat1").rows) == [
        {"tbl": "table", "idx": "idx_table_name", "stat": "2 1"}
    ]


def test_create_with_a_null_column(fresh_db):
    record = {"name": "Name", "description": None}
    fresh_db.table("t").insert(record)
    assert [record] == list(fresh_db.table("t").rows)


def test_create_with_nested_bytes(fresh_db):
    record = {"id": 1, "data": {"foo": b"bytes"}}
    fresh_db.table("t").insert(record)
    assert [{"id": 1, "data": '{"foo": "b\'bytes\'"}'}] == list(
        fresh_db.table("t").rows
    )


@pytest.mark.parametrize(
    "input,expected", [("hello", "'hello'"), ("hello'there'", "'hello''there'''")]
)
def test_quote(fresh_db, input, expected):
    assert fresh_db.quote(input) == expected


@pytest.mark.parametrize(
    "columns,expected_sql_middle",
    (
        (
            {"id": int},
            '"id" INTEGER',
        ),
        (
            {"col": dict},
            '"col" TEXT',
        ),
        (
            {"col": tuple},
            '"col" TEXT',
        ),
        (
            {"col": list},
            '"col" TEXT',
        ),
        (
            {"col": ANY},
            '"col" ANY',
        ),
        (
            {"col": "ANY"},
            '"col" ANY',
        ),
        (
            {"col": "any"},
            '"col" ANY',
        ),
    ),
)
def test_create_table_sql(fresh_db, columns, expected_sql_middle):
    sql = fresh_db.create_table_sql("t", columns)
    middle = sql.split("(")[1].split(")")[0].strip()
    assert middle == expected_sql_middle


def test_create(fresh_db):
    fresh_db.table("t").create(
        {
            "id": int,
            "text": str,
            "float": float,
            "integer": int,
            "bytes": bytes,
        },
        pk="id",
        column_order=("id", "float"),
        not_null=("float", "integer"),
        defaults={"integer": 0},
    )
    assert fresh_db.table("t").schema == (
        'CREATE TABLE "t" (\n'
        '   "id" INTEGER PRIMARY KEY,\n'
        '   "float" REAL NOT NULL,\n'
        '   "text" TEXT,\n'
        '   "integer" INTEGER NOT NULL DEFAULT 0,\n'
        '   "bytes" BLOB\n'
        ")"
    )


def test_create_if_not_exists(fresh_db):
    fresh_db.table("t").create({"id": int})
    # This should error
    with pytest.raises(sqlite3.OperationalError):
        fresh_db.table("t").create({"id": int})
    # This should not
    fresh_db.table("t").create({"id": int}, if_not_exists=True)


def test_create_if_no_columns(fresh_db):
    with pytest.raises(ValueError) as error:
        fresh_db.table("t").create({})
    assert error.value.args[0] == "Tables must have at least one column"


def test_create_ignore(fresh_db):
    fresh_db.table("t").create({"id": int})
    # This should error
    with pytest.raises(sqlite3.OperationalError):
        fresh_db.table("t").create({"id": int})
    # This should not
    fresh_db.table("t").create({"id": int}, ignore=True)


def test_create_replace(fresh_db):
    fresh_db.table("t").create({"id": int})
    # This should error
    with pytest.raises(sqlite3.OperationalError):
        fresh_db.table("t").create({"id": int})
    # This should not
    fresh_db.table("t").create({"name": str}, replace=True)
    assert fresh_db.table("t").schema == ('CREATE TABLE "t" (\n' '   "name" TEXT\n' ")")


@pytest.mark.parametrize(
    "cols,kwargs,expected_schema,should_transform",
    (
        # Change nothing
        (
            {"id": int, "name": str},
            {"pk": "id"},
            'CREATE TABLE "demo" (\n   "id" INTEGER PRIMARY KEY,\n   "name" TEXT\n)',
            False,
        ),
        # Drop name column, remove primary key
        ({"id": int}, {}, 'CREATE TABLE "demo" (\n   "id" INTEGER\n)', True),
        # Add a new column
        (
            {"id": int, "name": str, "age": int},
            {"pk": "id"},
            'CREATE TABLE "demo" (\n   "id" INTEGER PRIMARY KEY,\n   "name" TEXT,\n   "age" INTEGER\n)',
            True,
        ),
        # Change a column type
        (
            {"id": int, "name": bytes},
            {"pk": "id"},
            'CREATE TABLE "demo" (\n   "id" INTEGER PRIMARY KEY,\n   "name" BLOB\n)',
            True,
        ),
        # Change the primary key
        (
            {"id": int, "name": str},
            {"pk": "name"},
            'CREATE TABLE "demo" (\n   "id" INTEGER,\n   "name" TEXT PRIMARY KEY\n)',
            True,
        ),
        # Change in column order
        (
            {"id": int, "name": str},
            {"pk": "id", "column_order": ["name"]},
            'CREATE TABLE "demo" (\n   "name" TEXT,\n   "id" INTEGER PRIMARY KEY\n)',
            True,
        ),
        # Same column order is ignored
        (
            {"id": int, "name": str},
            {"pk": "id", "column_order": ["id", "name"]},
            'CREATE TABLE "demo" (\n   "id" INTEGER PRIMARY KEY,\n   "name" TEXT\n)',
            False,
        ),
        # Change not null
        (
            {"id": int, "name": str},
            {"pk": "id", "not_null": {"name"}},
            'CREATE TABLE "demo" (\n   "id" INTEGER PRIMARY KEY,\n   "name" TEXT NOT NULL\n)',
            True,
        ),
        # Change default values
        (
            {"id": int, "name": str},
            {"pk": "id", "defaults": {"id": 0, "name": "Bob"}},
            'CREATE TABLE "demo" (\n   "id" INTEGER PRIMARY KEY DEFAULT 0,\n   "name" TEXT DEFAULT \'Bob\'\n)',
            True,
        ),
    ),
)
def test_create_transform(fresh_db, cols, kwargs, expected_schema, should_transform):
    fresh_db.create_table("demo", {"id": int, "name": str}, pk="id")
    fresh_db.table("demo").insert({"id": 1, "name": "Cleo"})
    traces = []
    with fresh_db.tracer(lambda sql, parameters: traces.append((sql, parameters))):
        fresh_db.table("demo").create(cols, **kwargs, transform=True)
    at_least_one_create_table = any(sql.startswith("CREATE TABLE") for sql, _ in traces)
    assert should_transform == at_least_one_create_table
    new_schema = fresh_db.table("demo").schema
    assert new_schema == expected_schema, repr(new_schema)
    assert fresh_db.table("demo").count == 1


def test_create_transform_keyword_literal_defaults_unchanged(fresh_db):
    fresh_db.execute(
        "create table demo ("
        "id integer primary key, "
        "enabled integer default TRUE, "
        "disabled integer default FALSE, "
        "nullable text default NULL"
        ")"
    )
    traces = []
    with fresh_db.tracer(lambda sql, parameters: traces.append((sql, parameters))):
        fresh_db.table("demo").create(
            {"id": int, "enabled": int, "disabled": int, "nullable": str},
            pk="id",
            defaults={"enabled": True, "disabled": False, "nullable": None},
            transform=True,
        )
    assert not any(sql.startswith("CREATE TABLE") for sql, _ in traces)


def test_rename_table(fresh_db):
    fresh_db.table("t").insert({"foo": "bar"})
    assert ["t"] == fresh_db.table_names()
    fresh_db.rename_table("t", "renamed")
    assert ["renamed"] == fresh_db.table_names()
    assert [{"foo": "bar"}] == list(fresh_db.table("renamed").rows)
    # Should error if table does not exist:
    with pytest.raises(sqlite3.OperationalError):
        fresh_db.rename_table("does_not_exist", "renamed")


@pytest.mark.parametrize("strict", (False, True))
def test_database_strict(strict):
    db = Database(memory=True, strict=strict)
    table = db.table("t", columns={"id": int})
    table.insert({"id": 1})
    assert table.strict == strict or not db.supports_strict


@pytest.mark.parametrize("strict", (False, True))
def test_database_strict_override(strict):
    db = Database(memory=True, strict=strict)
    table = db.table("t", columns={"id": int}, strict=not strict)
    table.insert({"id": 1})
    assert table.strict != strict or not db.supports_strict


@pytest.mark.parametrize(
    "method_name", ("insert", "upsert", "insert_all", "upsert_all")
)
@pytest.mark.parametrize("strict", (False, True))
def test_insert_upsert_strict(fresh_db, method_name, strict):
    table = fresh_db.table("t")
    method = getattr(table, method_name)
    record = {"id": 1}
    if method_name.endswith("_all"):
        record = [record]
    method(record, pk="id", strict=strict)
    assert table.strict == strict or not fresh_db.supports_strict


@pytest.mark.parametrize("strict", (False, True))
def test_create_table_strict(fresh_db, strict):
    table = fresh_db.create_table("t", {"id": int, "f": float}, strict=strict)
    assert table.strict == strict or not fresh_db.supports_strict
    expected_schema = 'CREATE TABLE "t" (\n' '   "id" INTEGER,\n' '   "f" REAL\n' ")"
    if strict and not fresh_db.supports_strict:
        return
    if strict:
        expected_schema = 'CREATE TABLE "t" (\n   "id" INTEGER,\n   "f" REAL\n) STRICT'
    assert table.schema == expected_schema


@pytest.mark.parametrize("strict", (False, True))
def test_create_strict(fresh_db, strict):
    table = fresh_db.table("t")
    table.create({"id": int}, strict=strict)
    assert table.strict == strict or not fresh_db.supports_strict


def test_create_strict_with_any(fresh_db):
    if not fresh_db.supports_strict:
        pytest.skip("SQLite version does not support strict tables")
    table = fresh_db.table("items").create(
        {"id": int, "data": ANY}, pk="id", strict=True
    )
    table.insert_all(
        [
            {"id": 1, "data": 42},
            {"id": 2, "data": "000123"},
            {"id": 3, "data": 3.14},
            {"id": 4, "data": b"bytes"},
            {"id": 5, "data": None},
        ]
    )
    assert table.columns_dict == {"id": int, "data": ANY}
    assert fresh_db.execute(
        "select typeof(data), data from items order by id"
    ).fetchall() == [
        ("integer", 42),
        ("text", "000123"),
        ("real", 3.14),
        ("blob", b"bytes"),
        ("null", None),
    ]


def test_bad_table_and_view_exceptions(fresh_db):
    fresh_db.table("t").insert({"id": 1}, pk="id")
    fresh_db.create_view("v", "select * from t")
    with pytest.raises(NoTable) as ex:
        fresh_db.table("v")
    assert ex.value.args[0] == "Table v is actually a view"
    with pytest.raises(NoView) as ex2:
        fresh_db.view("t")
    assert ex2.value.args[0] == "View t does not exist - t is a table"
    with pytest.raises(NoView) as ex3:
        fresh_db.view("missing")
    assert ex3.value.args[0] == "View missing does not exist"


# Tests for issue #655: Table configuration should be stored in _defaults
# after table creation, so subsequent operations use the same settings.


def test_pk_persists_after_insert_655(fresh_db):
    """When pk is passed to insert(), subsequent inserts should use it."""
    table = fresh_db.table("users")
    table.insert({"id": 1, "name": "Alice"}, pk="id")
    # Second insert should use pk="id" from _defaults
    table.insert({"id": 2, "name": "Bob"})
    assert table.pks == ["id"]
    # Verify both rows exist (not overwritten due to missing pk)
    assert table.count == 2


def test_pk_persists_after_insert_all_655(fresh_db):
    """When pk is passed to insert_all(), subsequent inserts should use it."""
    table = fresh_db.table("users")
    table.insert_all([{"id": 1, "name": "Alice"}], pk="id")
    # Second insert_all should use pk="id" from _defaults
    table.insert_all([{"id": 2, "name": "Bob"}])
    assert table.pks == ["id"]
    assert table.count == 2


def test_pk_persists_after_create_655(fresh_db):
    """When pk is passed to create(), it should be stored in _defaults."""
    table = fresh_db.table("users")
    table.create({"id": int, "name": str}, pk="id")
    assert table._defaults["pk"] == "id"
    # Subsequent insert should use the pk
    table.insert({"id": 1, "name": "Alice"})
    table.insert({"id": 2, "name": "Bob"})
    assert table.count == 2


def test_foreign_keys_persist_after_create_655(fresh_db):
    """When foreign_keys is passed to create(), it should be stored in _defaults."""
    fresh_db.table("authors").insert({"id": 1, "name": "Alice"}, pk="id")
    table = fresh_db.table("books")
    table.create(
        {"id": int, "title": str, "author_id": int},
        pk="id",
        foreign_keys=[("author_id", "authors", "id")],
    )
    assert table._defaults["pk"] == "id"
    assert table._defaults["foreign_keys"] == [("author_id", "authors", "id")]


def test_not_null_persists_after_create_655(fresh_db):
    """When not_null is passed to create(), it should be stored in _defaults."""
    table = fresh_db.table("users")
    table.create({"id": int, "name": str}, pk="id", not_null=["name"])
    assert table._defaults["not_null"] == ["name"]


def test_defaults_persist_after_create_655(fresh_db):
    """When defaults is passed to create(), it should be stored in _defaults."""
    table = fresh_db.table("users")
    table.create({"id": int, "score": int}, pk="id", defaults={"score": 0})
    assert table._defaults["defaults"] == {"score": 0}


def test_strict_persists_after_create_655(fresh_db):
    """When strict is passed to create(), it should be stored in _defaults."""
    table = fresh_db.table("users")
    table.create({"id": int, "name": str}, pk="id", strict=True)
    assert table._defaults["strict"] is True


def test_upsert_uses_pk_from_prior_insert_655(fresh_db):
    """After insert with pk, upsert should use the same pk."""
    table = fresh_db.table("users")
    table.insert({"id": 1, "name": "Alice"}, pk="id")
    # Upsert should work without specifying pk again
    table.upsert({"id": 1, "name": "Alice Updated"})
    assert table.count == 1
    assert next(iter(table.rows))["name"] == "Alice Updated"


def test_upsert_all_uses_pk_from_prior_insert_655(fresh_db):
    """After insert with pk, upsert_all should use the same pk."""
    table = fresh_db.table("users")
    table.insert({"id": 1, "name": "Alice"}, pk="id")
    # Upsert_all should work without specifying pk again
    table.upsert_all([{"id": 1, "name": "Alice Updated"}, {"id": 2, "name": "Bob"}])
    assert table.count == 2
    rows = {row["id"]: row["name"] for row in table.rows}
    assert rows == {1: "Alice Updated", 2: "Bob"}


def test_chained_create_sets_pks(fresh_db):
    table = fresh_db.table("dogs3", pk="id").create(
        {"id": int, "name": str, "color": str}
    )
    assert table.pks == ["id"]
