from typing import Optional

import pytest
from _pytest.outcomes import fail
from sqlalchemy import Column, ForeignKeyConstraint
from sqlalchemy.orm import sessionmaker
from sqlmodel import Field

from daomodel import DAOModel
from daomodel.dao import NotFound, DAO
from daomodel.db import DAOFactory, create_engine, init_db
from daomodel.util import next_id


class Person(DAOModel, table=True):
    name: str = Field(primary_key=True)
    age: int = Field(primary_key=True)
    ssn: Optional[str]

    @classmethod
    def get_searchable_properties(cls) -> set[Column]:
        return {cls.name, cls.age}


class Book(DAOModel, table=True):
    name: str = Field(primary_key=True)
    subject: str
    owner: int = Field(foreign_key="student.id")


class Hall(DAOModel, table=True):
    location: str = Field(primary_key=True)
    floor: int = Field(primary_key=True)
    color: str


class Locker(DAOModel, table=True):
    __table_args__ = (
        ForeignKeyConstraint(
            ["location", "floor"],
            ["hall.location", "hall.floor"],
        ),
    )

    number: int = Field(primary_key=True)
    owner: int = Field(foreign_key="student.id")
    location: str
    floor: int


class Student(DAOModel, table=True):
    id: int = Field(primary_key=True)
    name: Optional[str]
    gender: Optional[str]
    active: bool = Field(default=True)

    @classmethod
    def get_searchable_properties(cls) -> set[Column|tuple[DAOModel, ..., Column]]:
        return {cls.id, cls.name, cls.gender, cls.active,
                Book.name, Book.subject, Locker.number,
                (Locker, Hall.color)}


class TestDAOFactory(DAOFactory):
    """
    A DAOFactory specifically designed for pytest.
    Includes functionality that can assert what is commited within the DB (through a secondary Session).
    """
    def __init__(self):
        engine = create_engine()  # create_engine("test.db")  # See DB for debugging, must delete file to rerun
        init_db(engine)
        super().__init__(sessionmaker(bind=engine))

    def __enter__(self):
        super().__enter__()
        self.assert_session = self.session_factory()
        return self

    def assert_in_db(self, model: type[DAOModel], *pk, **expected_values) -> None:
        """
        Assert that an object with specific attribute values is present in the DB.

        :param model: The DB table to check
        :param pk: The primary key values of the row
        :param expected_values: The column values to assert
        """
        try:
            persisted_copy = DAO(model, self.assert_session).get(*pk)
            for key, expected in expected_values.items():
                actual = getattr(persisted_copy, key)
                assert actual == expected, f"expected {key} of {persisted_copy} to be {expected} but was {actual}"
        except NotFound as e:
            fail(e.detail)

    def assert_not_in_db(self, model: type[DAOModel], *pk) -> None:
        """
        Assert that the specified object is not present in the DB.

        :param model: The DB table to check
        :param pk: The primary key values of the row
        """
        with pytest.raises(NotFound):
            DAO(model, self.assert_session).get(*pk)

    def __exit__(self, exc_type, exc_value, traceback):
        super().__exit__(exc_type, exc_value, traceback)
        self.assert_session.close()


@pytest.fixture(name="daos")
def daos_fixture() -> TestDAOFactory:
    """
    Provides a DAOFactory for Testing as a pytest fixture named `daos`.
    """
    with TestDAOFactory() as daos:
        yield daos


@pytest.fixture(name="person_dao")
def person_dao_fixture(daos: TestDAOFactory) -> DAO:
    dao = daos[Person]
    dao.create_with(name="John", age=23)
    dao.create_with(name="Joe", age=32)
    dao.create_with(name="Mike", age=25)
    dao.create_with(name="John", age=45)
    dao.create_with(name="Greg", age=31)
    dao.create_with(name="Mike", age=18)
    dao.create_with(name="Paul", age=25)
    return dao


pk_ordered = [
    Person(name="Greg", age=31),
    Person(name="Joe", age=32),
    Person(name="John", age=23),
    Person(name="John", age=45),
    Person(name="Mike", age=18),
    Person(name="Mike", age=25),
    Person(name="Paul", age=25)
]
age_ordered = [
    Person(name="Mike", age=18),
    Person(name="John", age=23),
    Person(name="Mike", age=25),
    Person(name="Paul", age=25),
    Person(name="Greg", age=31),
    Person(name="Joe", age=32),
    Person(name="John", age=45)
]
duplicated_names = [
    Person(name="John", age=23),
    Person(name="John", age=45),
    Person(name="Mike", age=18),
    Person(name="Mike", age=25)
]
unique_names = [
    Person(name="Greg", age=31),
    Person(name="Joe", age=32),
    Person(name="Paul", age=25)
]


def setup_students(daos: TestDAOFactory) -> DAO:
    dao = daos[Student]
    dao.create_with(id=100, name="Bob", gender="m")
    dao.create_with(id=next_id(), name="Mike", gender="m")
    dao.create_with(id=next_id(), name="John", gender="m", active=False)
    dao.create_with(id=next_id(), name="Jerry", gender="m")
    dao.create_with(id=next_id(), name="Carl", gender="m")
    dao.create_with(id=next_id(), name="Barbara", gender="f")
    dao.create_with(id=next_id(), name="Mary", gender="f", active=False)
    dao.create_with(id=next_id(), name="Jill", gender="f", active=False)
    dao.create_with(id=next_id(), name="Jessie", gender="f")
    dao.create_with(id=next_id(), name="Karen", gender="f")
    dao.create_with(id=next_id(), name="Sam")
    dao.create_with(id=next_id(), active=False)
    dao.create(next_id())
    return dao


@pytest.fixture(name="student_dao")
def student_dao_fixture(daos: TestDAOFactory) -> DAO:
    return setup_students(daos)


@pytest.fixture(name="school_dao")
def school_dao_fixture(daos: TestDAOFactory) -> DAO:
    dao = setup_students(daos)
    book_dao = daos[Book]
    book_dao.create_with(name="Biology 101", subject="Science", owner=100)
    book_dao.create_with(name="Art of the World", subject="Art", owner=101)
    book_dao.create_with(name="Pre-Calculus", subject="Math", owner=102)
    book_dao.create_with(name="Calculus", subject="Math", owner=103)
    hall_dao = daos[Hall]
    hall_dao.create_with(location="EAST", floor=1, color="green")
    hall_dao.create_with(location="WEST", floor=1, color="red")
    hall_dao.create_with(location="EAST", floor=2, color="blue")
    hall_dao.create_with(location="WEST", floor=2, color="yellow")
    locker_dao = daos[Locker]
    locker_dao.create_with(number=1101, owner=100, location="EAST", floor=1)
    locker_dao.create_with(number=1102, owner=108, location="EAST", floor=1)
    locker_dao.create_with(number=1103, owner=103, location="EAST", floor=1)
    locker_dao.create_with(number=1104, owner=104, location="WEST", floor=1)
    locker_dao.create_with(number=1105, owner=109, location="WEST", floor=1)
    locker_dao.create_with(number=1106, owner=101, location="WEST", floor=1)
    locker_dao.create_with(number=1201, owner=107, location="EAST", floor=2)
    locker_dao.create_with(number=1202, owner=110, location="EAST", floor=2)
    locker_dao.create_with(number=1203, owner=102, location="EAST", floor=2)
    locker_dao.create_with(number=1204, owner=106, location="WEST", floor=2)
    locker_dao.create_with(number=1205, owner=111, location="WEST", floor=2)
    locker_dao.create_with(number=1206, owner=105, location="WEST", floor=2)
    return dao


def split(to_split: list, target_size: int) -> tuple[list, ...]:
    return tuple(to_split[x:x+target_size] for x in range(0, len(to_split), target_size))


all_students = [
    Student(id=100),
    Student(id=101),
    Student(id=102),
    Student(id=103),
    Student(id=104),
    Student(id=105),
    Student(id=106),
    Student(id=107),
    Student(id=108),
    Student(id=109),
    Student(id=110),
    Student(id=111),
    Student(id=112)
]
active = [
    Student(id=100),
    Student(id=101),
    Student(id=103),
    Student(id=104),
    Student(id=105),
    Student(id=108),
    Student(id=109),
    Student(id=110),
    Student(id=112)
]
inactive = [
    Student(id=102),
    Student(id=106),
    Student(id=107),
    Student(id=111)
]
active_females = [
    Student(id=105),
    Student(id=108),
    Student(id=109)
]
having_gender = [
    Student(id=100),
    Student(id=101),
    Student(id=102),
    Student(id=103),
    Student(id=104),
    Student(id=105),
    Student(id=106),
    Student(id=107),
    Student(id=108),
    Student(id=109)
]
not_having_name = [
    Student(id=111),
    Student(id=112)
]
page_one, page_two, page_three = split(all_students, 5)
