import typing
from typing import Optional

import pytest
from pydal.objects import Query

from src.typedal import Relationship, TypeDAL, TypedField, TypedTable, relationship

# db = TypeDAL("sqlite:memory")
db = TypeDAL("sqlite://debug.db")


@db.define()
class TestQueryTable(TypedTable):
    number: TypedField[int]
    other = TypedField(str, default="Something")
    yet_another = TypedField(list[str], default=["something", "and", "other", "things"])

    relations = relationship(list["TestRelationship"], condition=lambda self, other: self.id == other.querytable)


@db.define()
class TestRelationship(TypedTable):
    name: TypedField[str]
    value: TypedField[int]

    querytable: TestQueryTable


class Undefined(TypedTable):
    value: int


def test_repr_unbound():
    assert "unbound table Undefined" in str(Undefined)


def test_query_type():
    assert isinstance(TestQueryTable.number > 3, Query)
    assert isinstance(TestQueryTable.number >= 3, Query)
    assert isinstance(TestQueryTable.number == 3, Query)
    assert isinstance(TestQueryTable.number < 3, Query)
    assert isinstance(TestQueryTable.number <= 3, Query)
    assert isinstance(TestQueryTable.number != 3, Query)


def _setup_data():
    TestQueryTable.truncate()
    first = TestQueryTable.insert(number=0)
    second = TestQueryTable.insert(number=1)
    TestQueryTable.insert(number=2)
    TestQueryTable.insert(number=3)
    TestQueryTable.insert(number=4)

    TestRelationship.insert(name="First Relation", querytable=first, value=3)
    TestRelationship.insert(name="Second Relation", querytable=first, value=3)
    TestRelationship.insert(name="Third Relation", querytable=first, value=3)
    TestRelationship.insert(name="Fourth Relation", querytable=first, value=3)

    TestRelationship.insert(name="First Relation", querytable=second, value=33)
    TestRelationship.insert(name="Second Relation", querytable=second, value=33)
    TestRelationship.insert(name="Third Relation", querytable=second, value=33)
    TestRelationship.insert(name="Fourth Relation", querytable=second, value=33)

    db.commit()


def test_where_builder():
    _setup_data()

    assert TestQueryTable.first().id == TestQueryTable.select().first().id

    builder = TestQueryTable.where(lambda row: row.number < 3).where(TestQueryTable.number > 1)
    results = builder.collect()
    assert len(results) == 1

    sql = builder._first()
    assert "LIMIT 1" in sql

    instance = results.first()

    assert instance.number == 2
    assert instance.other == "Something"

    assert isinstance(instance, TestQueryTable)

    results = builder.select(TestQueryTable.id, TestQueryTable.number).first()

    assert "number" in TestQueryTable
    assert "number" in results.__dict__
    assert "number" in results
    assert results.number == results["number"] == 2

    assert "other" in TestQueryTable
    assert "other" not in results.__dict__
    assert "other" not in results
    assert not results.other

    with pytest.raises(KeyError):
        assert not results["other"]

    assert not results.yet_another

    # delete all numbers above 2
    builder = TestQueryTable.where(lambda row: row.number > 2)
    assert builder._delete()
    assert isinstance(builder._delete(), str)

    result = builder.delete()
    assert len(result) == 2  # -> 3 and 4

    assert TestQueryTable.where(lambda row: row.number > 99).delete() is None  # nothing deleted

    assert TestQueryTable.count() == 3 == len(TestQueryTable.collect())  # 0 - 2

    builder = TestQueryTable.where(lambda row: row.number == 0)
    sql = builder._update(number=5)
    assert sql
    assert isinstance(sql, str)
    assert "number" in sql
    assert "5" in sql

    result = builder.update(number=5)
    assert TestQueryTable.where(lambda row: row.number == -1).update(number=5) is None  # nothing updated
    assert result == [1]  # id 1 updated

    assert TestQueryTable(1).number == 5

    success = TestQueryTable.select().collect_or_fail()
    assert len(success) == 3

    # test OR

    assert TestQueryTable.where(lambda row: row.id == 1, lambda row: row.id == 2).count() == 2
    assert TestQueryTable.where(lambda row: row.id == 1, lambda row: row.id == 99).count() == 1

    assert TestQueryTable.where(id=-1).first() is None
    with pytest.raises(ValueError):
        TestQueryTable.where(id=-1).collect_or_fail()

    with pytest.raises(TypeError):
        TestQueryTable.where(id=-1).collect_or_fail(TypeError("pytest"))

    with pytest.raises(ValueError):
        assert not TestQueryTable.where(id=-1).first_or_fail()

    with pytest.raises(TypeError):
        assert not TestQueryTable.where(id=-1).first_or_fail(TypeError("pytest"))

    # try to break stuff:

    with pytest.raises(ValueError):
        # illegal query:
        builder.where(Exception())

    with pytest.raises(EnvironmentError):
        # can't collect before defining on db!
        Undefined.collect()


def test_select():
    _setup_data()

    # all:
    full = TestQueryTable.where(lambda row: row.number > 0).join().select().first_or_fail()

    assert full.number
    assert full.other
    assert full.yet_another

    assert full.relations[0].name
    assert full.relations[0].value

    # specific fields:
    partial = (
        TestQueryTable.where(lambda row: row.number > 0)
        .join()
        .select(TestQueryTable.other, TestRelationship.value)
        .first_or_fail()
    )

    assert partial.other
    assert not partial.number
    assert not partial.yet_another

    assert not partial.relations[0].name
    assert partial.relations[0].value


def test_paginate():
    _setup_data()

    result = TestQueryTable.join(method="left").paginate(limit=1, page=1)

    result_two = result.next().previous()

    assert len(result) == 1 == len(result_two)
    assert len(result.first().relations) == 4 == len(result_two.first().relations)

    meta = result.metadata["pagination"]
    assert meta == result_two.metadata["pagination"]

    assert meta["current_page"] == 1
    assert meta["limit"] == 1
    assert meta["min_max"] == (0, 1)

    next_page = result.next()
    result = TestQueryTable.join(method="left").paginate(limit=1, page=2)

    assert len(result) == 1 == len(next_page)
    assert len(result.first().relations) == 4 == len(next_page.first().relations)

    meta = result.metadata["pagination"]
    assert meta == next_page.metadata["pagination"]

    assert meta["current_page"] == 2
    assert meta["limit"] == 1
    assert meta["min_max"] == (1, 2)

    next_page = result.next()

    sql = TestQueryTable.join(method="left")._paginate(limit=1, page=3)

    assert "LIMIT 1" in sql
    assert "OFFSET 2" in sql

    result = TestQueryTable.join(method="left").paginate(limit=1, page=3)

    assert len(result) == 1 == len(next_page) == len(result.data)
    assert len(result.first().relations) == 0 == len(next_page.first().relations)

    meta = result.metadata["pagination"]
    assert meta == next_page.metadata["pagination"]

    assert meta["current_page"] == 3
    assert meta["limit"] == 1
    assert meta["min_max"] == (2, 3)

    # final page (all items on 1 page is no prev or next):
    result = TestQueryTable.paginate(limit=10, page=1)

    with pytest.raises(StopIteration):
        result.previous()

    with pytest.raises(StopIteration):
        result.next()

    page_dict = result.as_dict()["pagination"]

    assert page_dict["has_next_page"] is False
    assert page_dict["has_prev_page"] is False
    assert page_dict["next_page"] is None
    assert page_dict["prev_page"] is None


def test_chunking():
    _setup_data()

    total = 0
    size = 3
    for rows in TestQueryTable.chunk(size):
        assert rows
        assert len(rows) <= size
        total += len(rows)

    assert total == TestQueryTable.count()


def test_complex_join():
    _setup_data()

    # only rows that ahve a TestQueryTable with number == 1
    builder = TestRelationship.join(
        "test_query_table",
        method="inner",
        condition=(TestRelationship.querytable == TestQueryTable.id) & (TestQueryTable.number == 1),
    )

    assert builder.count() == 4 == len(builder.collect())

    assert builder.collect()

    # notation 2:

    builder = TestRelationship.join(
        TestQueryTable,
        method="inner",
        condition=lambda relation, query: (relation.querytable == query.id) & (query.number == 1),
    )

    assert builder.count() == 4 == len(builder.collect())

    sql = builder._collect()
    assert "JOIN" in sql
    assert "LEFT" not in sql
    assert "CROSS" not in sql

    rows = builder.collect()

    # value = 3 only ahppens for querytable.number == 0
    assert len(rows) == 4
    assert set(rows.column("value")) == {33}

    for row in rows:
        # note: other column name since it's a manual relationship:
        assert row.test_query_table.number == 1

    page = builder.paginate(limit=2)

    assert set(page.column("value")) == {33}
    assert len(page) == 2

    page = page.next()
    assert len(page) == 2
    assert set(page.column("value")) == {33}

    with pytest.raises(StopIteration):
        assert not page.next()

    # custom .on:

    builder = TestRelationship.join(
        TestQueryTable,
        on=lambda relation, query: query.on((relation.querytable == query.id) & (query.number == 1)),
    )

    sql = builder._collect()
    assert "JOIN" in sql
    assert "LEFT" in sql
    assert "INNER" not in sql
    assert "CROSS" not in sql

    assert builder.count() == 8 == len(builder.collect())  # all but with less extra's

    # notation 2:

    builder = TestRelationship.join(
        TestQueryTable,
        on=TestQueryTable.on((TestRelationship.querytable == TestQueryTable.id) & (TestQueryTable.number == 1)),
    )

    assert builder.count() == 8 == len(builder.collect())  # all but with less extra's

    for row in builder.collect():
        if row.test_query_table:
            assert row.test_query_table.number == 1

    # value errors:

    with pytest.raises(ValueError):
        TestRelationship.join(
            TestQueryTable,
            TestRelationship,
            condition=lambda relation, query: (relation.querytable == query.id) & (query.number == 1),
        )

    with pytest.raises(ValueError):
        TestRelationship.join(
            TestQueryTable,
            TestRelationship,
            method="inner",
            on=lambda relation, query: (relation.querytable == query.id) & (query.number == 1),
        )

    with pytest.raises(ValueError):
        TestRelationship.join(
            TestQueryTable,
            method="inner",
            condition=lambda relation, query: (relation.querytable == query.id) & (query.number == 1),
            on=lambda relation, query: (relation.querytable == query.id) & (query.number == 1),
        )


def test_reprs_and_bool():
    assert TestQueryTable.where(id=1)
    assert not TestQueryTable.where(id=101)

    assert repr(TestQueryTable.where(id=1))
    assert str(TestQueryTable.where(id=1))
