import os
import pytest
from tinydb import Query

from db.db import (
    MongoLockedTable,
    _query_to_mongo_filter,
    child_db,
    task_db,
    users_db,
    refresh_tokens_db,
)
from db.mongo_client import get_mongo_client, get_mongo_db_name


# All tests in this module require the mongomock-backed MongoDB adapter.
pytestmark = pytest.mark.skipif(
    os.environ.get('USE_MONGODB', 'true').lower() != 'true',
    reason='MongoDB adapter tests require USE_MONGODB=true',
)


@pytest.fixture(autouse=True)
def clean_mongo_collections():
    """Truncate relevant collections before each test."""
    child_db.truncate()
    task_db.truncate()
    users_db.truncate()
    refresh_tokens_db.truncate()
    yield


class TestQueryTranslation:
    """Unit tests for TinyDB Query -> MongoDB filter translation."""

    def test_simple_equality(self):
        q = Query()
        assert _query_to_mongo_filter(q.id == 'abc') == {'_id': {'$eq': 'abc'}}

    def test_field_other_than_id(self):
        q = Query()
        assert _query_to_mongo_filter(q.user_id == 'u1') == {
            'user_id': {'$eq': 'u1'}
        }

    def test_and_query(self):
        q = Query()
        mongo_filter = _query_to_mongo_filter(
            (q.id == 'abc') & (q.user_id == 'u1')
        )
        assert mongo_filter == {'_id': {'$eq': 'abc'}, 'user_id': {'$eq': 'u1'}}

    def test_or_query(self):
        q = Query()
        mongo_filter = _query_to_mongo_filter(
            (q.user_id == 'u1') | (q.user_id == None)  # noqa: E711
        )
        assert mongo_filter == {
            '$or': [
                {'user_id': {'$eq': 'u1'}},
                {'user_id': {'$eq': None}},
            ]
        }

    def test_and_with_nested_or(self):
        q = Query()
        mongo_filter = _query_to_mongo_filter(
            (q.id == 'abc') & ((q.user_id == 'u1') | (q.user_id == None))  # noqa: E711
        )
        assert mongo_filter == {
            '_id': {'$eq': 'abc'},
            '$or': [
                {'user_id': {'$eq': 'u1'}},
                {'user_id': {'$eq': None}},
            ],
        }


class TestMongoCrud:
    """CRUD tests against the mongomock-backed MongoLockedTable."""

    def test_insert_maps_id_to_underscore_id(self):
        child_db.insert({'id': 'c1', 'name': 'Alice', 'age': 8})

        raw = get_mongo_client()[get_mongo_db_name()]['children'].find_one(
            {'_id': 'c1'}
        )
        assert raw is not None
        assert raw['_id'] == 'c1'
        assert 'id' not in raw
        assert raw['name'] == 'Alice'

    def test_get_returns_document_without_underscore_id(self):
        child_db.insert({'id': 'c1', 'name': 'Alice', 'age': 8})

        doc = child_db.get(Query().id == 'c1')
        assert doc is not None
        assert doc['id'] == 'c1'
        assert doc['name'] == 'Alice'
        assert '_id' not in doc

    def test_get_none_when_missing(self):
        assert child_db.get(Query().id == 'missing') is None

    def test_search_with_query(self):
        child_db.insert({'id': 'c1', 'name': 'Alice', 'user_id': 'u1'})
        child_db.insert({'id': 'c2', 'name': 'Bob', 'user_id': 'u1'})
        child_db.insert({'id': 'c3', 'name': 'Carol', 'user_id': 'u2'})

        results = child_db.search(Query().user_id == 'u1')
        assert len(results) == 2
        assert {r['id'] for r in results} == {'c1', 'c2'}

    def test_search_with_or(self):
        task_db.insert({'id': 't1', 'name': 'Default', 'user_id': None})
        task_db.insert({'id': 't2', 'name': 'User task', 'user_id': 'u1'})

        q = Query()
        results = task_db.search((q.user_id == 'u1') | (q.user_id == None))  # noqa: E711
        assert len(results) == 2

    def test_update_modifies_matching_documents(self):
        child_db.insert({'id': 'c1', 'name': 'Alice', 'points': 0})
        child_db.insert({'id': 'c2', 'name': 'Bob', 'points': 0})

        modified = child_db.update({'points': 10}, Query().id == 'c1')
        # TinyDB returns a list of updated document ids; the adapter mirrors that.
        assert modified == ['c1']

        doc = child_db.get(Query().id == 'c1')
        assert doc['points'] == 10

        other = child_db.get(Query().id == 'c2')
        assert other['points'] == 0

    def test_update_does_not_overwrite_id(self):
        child_db.insert({'id': 'c1', 'name': 'Alice'})

        child_db.update({'id': 'c2', 'name': 'Alice Smith'}, Query().id == 'c1')
        # The id field must remain unchanged; update should have stripped id.
        assert child_db.get(Query().id == 'c1')['name'] == 'Alice Smith'
        assert child_db.get(Query().id == 'c2') is None

    def test_remove_deletes_matching_documents(self):
        child_db.insert({'id': 'c1', 'name': 'Alice'})
        child_db.insert({'id': 'c2', 'name': 'Bob'})

        deleted = child_db.remove(Query().id == 'c1')
        # TinyDB returns a list of removed document ids; the adapter mirrors that.
        assert deleted == ['c1']

        assert child_db.get(Query().id == 'c1') is None
        assert child_db.get(Query().id == 'c2') is not None

    def test_all_returns_all_documents(self):
        child_db.insert({'id': 'c1', 'name': 'Alice'})
        child_db.insert({'id': 'c2', 'name': 'Bob'})

        docs = child_db.all()
        assert len(docs) == 2
        assert all('_id' not in d for d in docs)

    def test_truncate_removes_all_documents(self):
        child_db.insert({'id': 'c1', 'name': 'Alice'})
        child_db.truncate()
        assert child_db.all() == []

    def test_insert_multiple(self):
        ids = child_db.insert_multiple([
            {'id': 'c1', 'name': 'Alice'},
            {'id': 'c2', 'name': 'Bob'},
        ])
        assert sorted(ids) == ['c1', 'c2']
        assert len(child_db.all()) == 2

    def test_unique_token_index(self):
        refresh_tokens_db.insert({'id': 'r1', 'token': 'abc', 'user_id': 'u1'})
        refresh_tokens_db.insert({'id': 'r2', 'token': 'def', 'user_id': 'u1'})

        # mongomock does not enforce unique indexes by default, but we verify
        # both records are readable.
        assert refresh_tokens_db.get(Query().token == 'abc') is not None
        assert refresh_tokens_db.get(Query().token == 'def') is not None

    def test_user_id_secondary_index_is_created(self):
        # Insert and query via the secondary index path used by the app.
        users_db.insert({'id': 'u1', 'email': 'a@example.com', 'verified': True})
        users_db.insert({'id': 'u2', 'email': 'b@example.com', 'verified': False})

        found = users_db.search(Query().verified == True)  # noqa: E712
        assert len(found) == 1
        assert found[0]['id'] == 'u1'


class TestAdapterApi:
    """Tests that the adapter exposes the expected LockedTable-compatible API."""

    def test_close_is_noop(self):
        # Existing cleanup fixtures call ``*_db.close()``; ensure it does not
        # raise for the MongoDB-backed adapter.
        child_db.close()
