"""Tests for the unified GraphClient — mocked unit tests + integration tests."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest

from koan.graph.client import (
    FalkorDBClient,
    GraphRecord,
    Neo4jClient,
    create_graph_client,
)
from koan.graph.config import GraphConfig

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


def _neo4j_config() -> GraphConfig:
    return GraphConfig(
        provider="neo4j",
        uri="bolt://localhost:7687",
        database="testdb",
        username="neo4j",
        password="password",
    )


def _memgraph_config() -> GraphConfig:
    return GraphConfig(
        provider="memgraph",
        uri="bolt://localhost:7687",
        database="memgraph",
    )


def _falkordb_config() -> GraphConfig:
    return GraphConfig(
        provider="falkordb",
        uri="redis://localhost:6379",
        database="koan",
    )


# ---------------------------------------------------------------------------
# create_graph_client factory
# ---------------------------------------------------------------------------


class TestCreateGraphClient:
    def test_returns_neo4j_client_for_neo4j(self) -> None:
        client = create_graph_client(_neo4j_config())
        assert isinstance(client, Neo4jClient)

    def test_returns_neo4j_client_for_memgraph(self) -> None:
        """Memgraph uses Bolt — same client as Neo4j."""
        client = create_graph_client(_memgraph_config())
        assert isinstance(client, Neo4jClient)

    def test_returns_falkordb_client(self) -> None:
        client = create_graph_client(_falkordb_config())
        assert isinstance(client, FalkorDBClient)


# ---------------------------------------------------------------------------
# GraphConfig validation
# ---------------------------------------------------------------------------


class TestGraphConfig:
    def test_valid_neo4j_config(self) -> None:
        config = _neo4j_config()
        assert config.provider == "neo4j"
        assert config.uri == "bolt://localhost:7687"

    def test_valid_falkordb_config(self) -> None:
        config = _falkordb_config()
        assert config.provider == "falkordb"
        assert config.database == "koan"

    def test_defaults(self) -> None:
        config = GraphConfig(provider="neo4j", uri="bolt://localhost:7687")
        assert config.database == "koan"
        assert config.username is None
        assert config.password is None


# ---------------------------------------------------------------------------
# Neo4jClient (mocked — no real DB)
# ---------------------------------------------------------------------------


class TestNeo4jClientMocked:
    async def test_connect_creates_driver(self) -> None:
        client = Neo4jClient(_neo4j_config())
        client.connect = AsyncMock()  # type: ignore[method-assign]
        await client.connect()
        client.connect.assert_awaited_once()

    async def test_execute_returns_records(self) -> None:
        client = Neo4jClient(_neo4j_config())

        # Create an async iterable mock for the result
        rows = [{"n": "node1"}, {"n": "node2"}]

        async def _async_iter() -> AsyncMock:
            for row in rows:
                yield row

        mock_result = _async_iter()

        mock_session = AsyncMock()
        mock_session.run = AsyncMock(return_value=mock_result)
        mock_session.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session.__aexit__ = AsyncMock(return_value=False)

        mock_driver = MagicMock()
        mock_driver.session = MagicMock(return_value=mock_session)
        client._driver = mock_driver

        records = await client.execute("MATCH (n) RETURN n")
        assert len(records) == 2
        assert all(isinstance(r, GraphRecord) for r in records)

    async def test_close_calls_driver_close(self) -> None:
        client = Neo4jClient(_neo4j_config())
        mock_driver = AsyncMock()
        client._driver = mock_driver

        await client.close()
        mock_driver.close.assert_awaited_once()

    async def test_close_noop_when_no_driver(self) -> None:
        client = Neo4jClient(_neo4j_config())
        await client.close()  # Should not raise

    async def test_health_check_success(self) -> None:
        client = Neo4jClient(_neo4j_config())
        mock_driver = AsyncMock()
        client._driver = mock_driver

        result = await client.health_check()
        assert result is True
        mock_driver.verify_connectivity.assert_awaited_once()

    async def test_health_check_failure(self) -> None:
        client = Neo4jClient(_neo4j_config())
        mock_driver = AsyncMock()
        mock_driver.verify_connectivity = AsyncMock(side_effect=ConnectionError("refused"))
        client._driver = mock_driver

        result = await client.health_check()
        assert result is False

    async def test_context_manager(self) -> None:
        client = Neo4jClient(_neo4j_config())
        client.connect = AsyncMock()  # type: ignore[method-assign]
        client.close = AsyncMock()  # type: ignore[method-assign]

        async with client as c:
            assert c is client
        client.connect.assert_awaited_once()
        client.close.assert_awaited_once()


# ---------------------------------------------------------------------------
# FalkorDBClient (mocked — no real DB)
# ---------------------------------------------------------------------------


class TestFalkorDBClientMocked:
    def test_parse_host(self) -> None:
        client = FalkorDBClient(_falkordb_config())
        assert client._parse_host() == "localhost"

    def test_parse_port(self) -> None:
        client = FalkorDBClient(_falkordb_config())
        assert client._parse_port() == 6379

    def test_parse_port_default(self) -> None:
        config = GraphConfig(provider="falkordb", uri="redis://myhost")
        client = FalkorDBClient(config)
        assert client._parse_host() == "myhost"
        assert client._parse_port() == 6379

    async def test_close_calls_db_close(self) -> None:
        client = FalkorDBClient(_falkordb_config())
        mock_db = AsyncMock()
        client._db = mock_db

        await client.close()
        mock_db.aclose.assert_awaited_once()

    async def test_close_noop_when_no_db(self) -> None:
        client = FalkorDBClient(_falkordb_config())
        await client.close()  # Should not raise

    async def test_execute_returns_records(self) -> None:
        client = FalkorDBClient(_falkordb_config())

        mock_result = MagicMock()
        mock_result.header = ["name", "age"]
        mock_result.result_set = [["Alice", 30], ["Bob", 25]]

        mock_graph = AsyncMock()
        mock_graph.query = AsyncMock(return_value=mock_result)
        client._graph = mock_graph

        records = await client.execute("MATCH (n) RETURN n.name, n.age")
        assert len(records) == 2
        assert records[0].data == {"name": "Alice", "age": 30}
        assert records[1].data == {"name": "Bob", "age": 25}

    async def test_execute_write_delegates_to_execute(self) -> None:
        client = FalkorDBClient(_falkordb_config())

        mock_result = MagicMock()
        mock_result.header = []
        mock_result.result_set = []

        mock_graph = AsyncMock()
        mock_graph.query = AsyncMock(return_value=mock_result)
        client._graph = mock_graph

        records = await client.execute_write("CREATE (n:Test)")
        assert records == []

    async def test_health_check_success(self) -> None:
        client = FalkorDBClient(_falkordb_config())
        mock_graph = AsyncMock()
        client._graph = mock_graph

        result = await client.health_check()
        assert result is True

    async def test_health_check_failure(self) -> None:
        client = FalkorDBClient(_falkordb_config())
        mock_graph = AsyncMock()
        mock_graph.query = AsyncMock(side_effect=ConnectionError("refused"))
        client._graph = mock_graph

        result = await client.health_check()
        assert result is False


# ---------------------------------------------------------------------------
# GraphRecord
# ---------------------------------------------------------------------------


class TestGraphRecord:
    def test_default_data(self) -> None:
        record = GraphRecord()
        assert record.data == {}

    def test_with_data(self) -> None:
        record = GraphRecord(data={"name": "test", "count": 42})
        assert record.data["name"] == "test"


# ---------------------------------------------------------------------------
# Integration tests (require running DB — marked with @pytest.mark.graph)
# ---------------------------------------------------------------------------


@pytest.mark.graph
class TestNeo4jIntegration:
    """These tests require a running Neo4j instance.

    Start with: docker compose -f docker/docker-compose.graph.yml --profile neo4j up -d
    """

    async def test_connect_and_health_check(self) -> None:
        config = GraphConfig(
            provider="neo4j",
            uri="bolt://localhost:7687",
            database="neo4j",
            username="neo4j",
            password="koan-dev-password",
        )
        async with create_graph_client(config) as client:
            assert await client.health_check() is True

    async def test_execute_write_and_read(self) -> None:
        config = GraphConfig(
            provider="neo4j",
            uri="bolt://localhost:7687",
            database="neo4j",
            username="neo4j",
            password="koan-dev-password",
        )
        async with create_graph_client(config) as client:
            # Write
            await client.execute_write(
                "CREATE (n:KoanTest {name: $name}) RETURN n.name AS name",
                {"name": "integration-test"},
            )

            # Read
            records = await client.execute(
                "MATCH (n:KoanTest {name: $name}) RETURN n.name AS name",
                {"name": "integration-test"},
            )
            assert len(records) >= 1
            assert records[0].data["name"] == "integration-test"

            # Cleanup
            await client.execute_write("MATCH (n:KoanTest) DELETE n")


@pytest.mark.graph
class TestFalkorDBIntegration:
    """These tests require a running FalkorDB instance.

    Start with: docker compose -f docker/docker-compose.graph.yml --profile falkordb up -d
    """

    async def test_connect_and_health_check(self) -> None:
        config = GraphConfig(
            provider="falkordb",
            uri="redis://localhost:6379",
            database="koan_test",
        )
        async with create_graph_client(config) as client:
            assert await client.health_check() is True
