"""Tests for BaseRepository auto-wiring and lifecycle edge cases."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from repo_core import BaseRepository, DomainModel, hook


# ===========================================================================
# Auto-wiring: _backends triggers lazy client creation
# ===========================================================================

async def test_repo_with_db_autowired():
    """When _backends={"db"} and no explicit db is provided, auto-wiring
    should create a PostgresClient via create_postgres_client()."""

    class AutoRepo(BaseRepository[dict]):
        _backends = {"db"}

    with patch("repo_core.base.repository._get_db_qb") as mock_get_db_qb:
        mock_qb = MagicMock()
        mock_qb.client = AsyncMock()
        mock_get_db_qb.return_value = mock_qb

        repo = AutoRepo()
        # Accessing db property triggers auto-wiring
        qb = repo.db
        mock_get_db_qb.assert_called_once()
        assert qb is mock_qb


async def test_repo_without_backend_does_not_autowire():
    """When _backends does not include 'db', accessing db raises RuntimeError."""

    class NoDbRepo(BaseRepository[dict]):
        _backends = set()

    repo = NoDbRepo()
    with pytest.raises(RuntimeError, match="DB QueryBuilder"):
        _ = repo.db


async def test_repo_with_search_autowired():
    """When _backends={"search"}, auto-wiring creates an ElasticsearchClient."""

    class SearchOnlyRepo(BaseRepository[dict]):
        _backends = {"search"}

    with patch("repo_core.base.repository._get_search_qb") as mock_get_search_qb:
        mock_qb = MagicMock()
        mock_qb.client = AsyncMock()
        mock_get_search_qb.return_value = mock_qb

        repo = SearchOnlyRepo()
        qb = repo.search
        mock_get_search_qb.assert_called_once()
        assert qb is mock_qb


async def test_explicit_backends_bypass_autowiring(db_query, search_query):
    """When db/search are passed explicitly, auto-wiring is never called."""

    class AutoRepo(BaseRepository[dict]):
        _backends = {"db", "search"}

    with patch("repo_core.base.repository._get_db_qb") as mock_db, \
         patch("repo_core.base.repository._get_search_qb") as mock_search:
        repo = AutoRepo(db=db_query, search=search_query)
        # Access properties — should use explicit values, not auto-wire
        assert repo.db is db_query
        assert repo.search is search_query
        mock_db.assert_not_called()
        mock_search.assert_not_called()


# ===========================================================================
# db setter
# ===========================================================================

async def test_db_setter_replaces_querybuilder(db_query):
    repo = BaseRepository(db=db_query)
    new_qb = MagicMock()
    repo.db = new_qb
    assert repo.db is new_qb


async def test_search_setter_replaces_querybuilder(search_query):
    repo = BaseRepository(search=search_query)
    new_qb = MagicMock()
    repo.search = new_qb
    assert repo.search is new_qb


# ===========================================================================
# _query_builders internal
# ===========================================================================

async def test_query_builders_returns_db_when_needed(db_query):
    class Repo(BaseRepository[dict]):
        _backends = {"db"}

    repo = Repo(db=db_query)
    qbs = repo._query_builders()
    assert len(qbs) == 1
    assert qbs[0] is db_query


async def test_query_builders_returns_both_when_needed(db_query, search_query):
    class Repo(BaseRepository[dict]):
        _backends = {"db", "search"}

    repo = Repo(db=db_query, search=search_query)
    qbs = repo._query_builders()
    assert len(qbs) == 2


async def test_query_builders_with_explicit_but_not_needed(search_query):
    """Even if _backends doesn't declare search, explicitly provided
    search QueryBuilders are still returned."""
    repo = BaseRepository(search=search_query)
    repo._needs_db = False
    repo._needs_search = False
    qbs = repo._query_builders()
    # search should be included because it was explicitly provided
    assert search_query in qbs


# ===========================================================================
# Context manager with both backends
# ===========================================================================

async def test_context_manager_connect_disconnect_both(db_query, search_query):
    class Repo(BaseRepository[dict]):
        _backends = {"db", "search"}

    repo = Repo(db=db_query, search=search_query)
    async with repo:
        pass

    db_query.client.connect.assert_awaited_once()
    db_query.client.disconnect.assert_awaited_once()
    search_query.client.connect.assert_awaited_once()
    search_query.client.disconnect.assert_awaited_once()


# ===========================================================================
# _register_class_hooks with inheritance
# ===========================================================================

async def test_subclass_inherits_hooks():
    """A subclass that doesn't define any hooks shouldn't break."""

    class Parent(BaseRepository[dict]):
        _backends = set()

        @hook("before_create")
        async def on_create(self, data):
            pass

    class Child(Parent):
        pass

    # Child should not have duplicate hooks
    repo = Child()
    # The hook should be registered once per instance
    assert len(repo.hooks._handlers.get("before_create", [])) == 1


# ===========================================================================
# table / search_table reuse same client
# ===========================================================================

async def test_table_reuses_db_client(db_query):
    repo = BaseRepository(db=db_query)
    t1 = repo.table("users")
    t2 = repo.table("posts")
    # Both QueryBuilders share the same underlying client
    assert t1.client is t2.client
    assert t1.client is db_query.client


async def test_search_table_reuses_search_client(search_query):
    repo = BaseRepository(search=search_query)
    s1 = repo.search_table("articles")
    s2 = repo.search_table("comments")
    assert s1.client is s2.client
    assert s1.client is search_query.client


# ===========================================================================
# model=None behavior
# ===========================================================================

async def test_repo_without_model_returns_raw_dicts():
    """When model=None, CRUD methods return raw dicts."""
    repo = BaseRepository[dict]()

    assert repo._from_db({"x": 1}) == {"x": 1}
    assert repo._from_db(None) is None
    assert repo._from_db_many([{"x": 1}]) == [{"x": 1}]
    assert repo._from_search({"x": 1}) == {"x": 1}
    assert repo._from_search(None) is None
    assert repo._from_warehouse({"x": 1}) == {"x": 1}
    assert repo._from_warehouse(None) is None
