# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

from unittest.mock import AsyncMock, MagicMock

import pytest

from mirage.core.mongodb.client import (get_index_stats, get_indexes,
                                        get_validator, is_view, iter_documents,
                                        iter_inserts)


class _AsyncIter:

    def __init__(self, items):
        self._items = list(items)

    def __aiter__(self):
        return self

    async def __anext__(self):
        if not self._items:
            raise StopAsyncIteration
        return self._items.pop(0)


def _build_mock_client(docs):
    cursor = MagicMock()
    cursor.sort = MagicMock(return_value=cursor)
    cursor.batch_size = MagicMock(return_value=cursor)
    cursor.__aiter__ = lambda self: _AsyncIter(docs).__aiter__()
    col = MagicMock()
    col.find = MagicMock(return_value=cursor)
    db = MagicMock()
    db.__getitem__.return_value = col
    client = MagicMock()
    client.__getitem__.return_value = db
    return client, col, cursor


@pytest.mark.asyncio
async def test_iter_documents_yields_each_doc_in_order():
    docs = [{"_id": i, "v": i * 10} for i in range(3)]
    client, col, cursor = _build_mock_client(docs)
    out = []
    async for doc in iter_documents(client, "db1", "coll1", batch_size=100):
        out.append(doc)
    assert out == docs
    col.find.assert_called_once_with({}, None)
    cursor.batch_size.assert_called_once_with(100)
    cursor.sort.assert_not_called()


@pytest.mark.asyncio
async def test_iter_documents_applies_filter_projection_and_sort():
    docs = [{"_id": 1, "x": 5}]
    client, col, cursor = _build_mock_client(docs)
    out = []
    async for doc in iter_documents(client,
                                    "db1",
                                    "coll1",
                                    filter={"x": {
                                        "$gt": 0
                                    }},
                                    projection={"x": 1},
                                    sort=[("_id", -1)],
                                    batch_size=50):
        out.append(doc)
    assert out == docs
    col.find.assert_called_once_with({"x": {"$gt": 0}}, {"x": 1})
    cursor.sort.assert_called_once_with([("_id", -1)])
    cursor.batch_size.assert_called_once_with(50)


@pytest.mark.asyncio
async def test_iter_documents_empty_yields_nothing():
    client, _, _ = _build_mock_client([])
    out = []
    async for doc in iter_documents(client, "db1", "coll1"):
        out.append(doc)
    assert out == []


def _build_indexes_client(spec, indexes):
    spec_cursor = MagicMock()
    spec_cursor.__aiter__ = lambda self: _AsyncIter([spec]
                                                    if spec else []).__aiter__(
                                                    )
    idx_cursor = MagicMock()
    idx_cursor.__aiter__ = lambda self: _AsyncIter(indexes).__aiter__()
    col = MagicMock()
    col.list_indexes = AsyncMock(return_value=idx_cursor)
    db = MagicMock()
    db.list_collections = AsyncMock(return_value=spec_cursor)
    db.__getitem__.return_value = col
    client = MagicMock()
    client.__getitem__.return_value = db
    return client, col, db


@pytest.mark.asyncio
async def test_is_view_true_when_spec_type_view():
    client, _, db = _build_indexes_client(
        spec={
            "name": "myview",
            "type": "view"
        },
        indexes=[],
    )
    assert await is_view(client, "db1", "myview") is True
    db.list_collections.assert_awaited_once_with(filter={"name": "myview"})


@pytest.mark.asyncio
async def test_is_view_false_for_regular_collection():
    client, _, _ = _build_indexes_client(
        spec={
            "name": "coll1",
            "type": "collection"
        },
        indexes=[],
    )
    assert await is_view(client, "db1", "coll1") is False


@pytest.mark.asyncio
async def test_is_view_false_when_collection_absent():
    client, _, _ = _build_indexes_client(spec=None, indexes=[])
    assert await is_view(client, "db1", "missing") is False


@pytest.mark.asyncio
async def test_get_indexes_returns_indexes_for_collection():
    indexes = [{"name": "_id_", "key": {"_id": 1}}]
    client, col, db = _build_indexes_client(
        spec={
            "name": "coll1",
            "type": "collection"
        },
        indexes=indexes,
    )
    out = await get_indexes(client, "db1", "coll1")
    assert out == indexes
    db.list_collections.assert_awaited_once_with(filter={"name": "coll1"})
    col.list_indexes.assert_awaited_once_with()


def _build_validator_client(spec):
    spec_cursor = MagicMock()
    spec_cursor.__aiter__ = lambda self: _AsyncIter([spec]
                                                    if spec else []).__aiter__(
                                                    )
    db = MagicMock()
    db.list_collections = AsyncMock(return_value=spec_cursor)
    client = MagicMock()
    client.__getitem__.return_value = db
    return client, db


@pytest.mark.asyncio
async def test_get_validator_returns_json_schema_when_present():
    spec = {
        "name": "movies",
        "options": {
            "validator": {
                "$jsonSchema": {
                    "bsonType": "object",
                    "required": ["title"]
                }
            }
        }
    }
    client, db = _build_validator_client(spec)
    out = await get_validator(client, "db1", "movies")
    assert out == {"bsonType": "object", "required": ["title"]}
    db.list_collections.assert_awaited_once_with(filter={"name": "movies"})


@pytest.mark.asyncio
async def test_get_validator_returns_none_without_validator():
    client, _ = _build_validator_client({"name": "movies", "options": {}})
    assert await get_validator(client, "db1", "movies") is None


@pytest.mark.asyncio
async def test_get_validator_returns_none_when_collection_missing():
    client, _ = _build_validator_client(None)
    assert await get_validator(client, "db1", "ghost") is None


def _build_indexstats_client(rows):
    cursor = MagicMock()
    cursor.__aiter__ = lambda self: _AsyncIter(rows).__aiter__()
    col = MagicMock()
    col.aggregate = AsyncMock(return_value=cursor)
    db = MagicMock()
    db.__getitem__.return_value = col
    client = MagicMock()
    client.__getitem__.return_value = db
    return client, col


@pytest.mark.asyncio
async def test_get_index_stats_returns_map_keyed_by_name():
    rows = [
        {
            "name": "_id_",
            "accesses": {
                "ops": 1234,
                "since": "2026-01-01"
            }
        },
        {
            "name": "title_text",
            "accesses": {
                "ops": 5678,
                "since": "2026-02-01"
            }
        },
    ]
    client, col = _build_indexstats_client(rows)
    out = await get_index_stats(client, "db1", "coll1")
    assert out["title_text"] == {"ops": 5678, "since": "2026-02-01"}
    assert out["_id_"] == {"ops": 1234, "since": "2026-01-01"}
    col.aggregate.assert_awaited_once_with([{"$indexStats": {}}])


@pytest.mark.asyncio
async def test_get_index_stats_empty_when_view():
    client, _ = _build_indexstats_client([])
    out = await get_index_stats(client, "db1", "myview")
    assert out == {}


@pytest.mark.asyncio
async def test_get_indexes_returns_empty_for_view_without_listing():
    client, col, db = _build_indexes_client(
        spec={
            "name": "myview",
            "type": "view"
        },
        indexes=[],
    )
    out = await get_indexes(client, "db1", "myview")
    assert out == []
    db.list_collections.assert_awaited_once_with(filter={"name": "myview"})
    col.list_indexes.assert_not_called()


class _AsyncChangeStream:

    def __init__(self, changes):
        self._iter = _AsyncIter(changes)

    async def __aenter__(self):
        return self

    async def __aexit__(self, *exc):
        return False

    def __aiter__(self):
        return self._iter


def _build_watch_client(changes):
    stream = _AsyncChangeStream(changes)
    col = MagicMock()
    col.watch = AsyncMock(return_value=stream)
    db = MagicMock()
    db.__getitem__.return_value = col
    client = MagicMock()
    client.__getitem__.return_value = db
    return client, col


@pytest.mark.asyncio
async def test_iter_inserts_yields_full_documents():
    changes = [
        {
            "operationType": "insert",
            "fullDocument": {
                "_id": 1,
                "v": "a"
            }
        },
        {
            "operationType": "insert",
            "fullDocument": {
                "_id": 2,
                "v": "b"
            }
        },
    ]
    client, col = _build_watch_client(changes)
    out = []
    async for doc in iter_inserts(client, "db1", "coll1"):
        out.append(doc)
    assert out == [{"_id": 1, "v": "a"}, {"_id": 2, "v": "b"}]
    col.watch.assert_awaited_once_with([{
        "$match": {
            "operationType": "insert"
        }
    }])


@pytest.mark.asyncio
async def test_iter_inserts_skips_changes_without_full_document():
    changes = [
        {
            "operationType": "insert",
            "fullDocument": {
                "_id": 1
            }
        },
        {
            "operationType": "drop"
        },
        {
            "operationType": "insert",
            "fullDocument": {
                "_id": 2
            }
        },
    ]
    client, _ = _build_watch_client(changes)
    out = []
    async for doc in iter_inserts(client, "db1", "coll1"):
        out.append(doc)
    assert out == [{"_id": 1}, {"_id": 2}]
