"""Unit tests for DomainDictionaryService."""

from unittest.mock import MagicMock
from uuid import uuid4

import pytest
from sqlalchemy.dialects import postgresql

from src.db.models import DomainTermDictionary
from src.knowledge.domain_dictionary_service import DomainDictionaryService


class TestDomainDictionaryService:
    """Tests for DomainDictionaryService class."""

    def test_add_term(self):
        """Should add term to dictionary and commit."""
        mock_session = MagicMock()
        service = DomainDictionaryService(mock_session)

        entry = DomainTermDictionary(
            term_original="revenue",
            term_ja="売上",
            term_en="revenue",
            term_vi="doanh thu",
            source_language="en",
            domain_category="finance",
        )

        result = service.add_term(entry)

        mock_session.add.assert_called_once_with(entry)
        mock_session.commit.assert_called_once()
        assert result == entry

    def test_get_all_terms_unscoped(self):
        """With no node_id, should return query results without a filter."""
        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_terms = [MagicMock(term_original="revenue"), MagicMock(term_original="employee")]
        mock_query.all.return_value = mock_terms

        service = DomainDictionaryService(mock_session)
        result = service.get_all_terms()

        mock_session.query.assert_called_once()
        mock_query.filter.assert_not_called()
        assert result == mock_terms

    def test_get_all_terms_scoped_by_argument(self, sample_node_id):
        """Explicit node_id argument should add a filter clause."""
        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.all.return_value = []

        service = DomainDictionaryService(mock_session)
        service.get_all_terms(node_id=sample_node_id)

        mock_query.filter.assert_called_once()

    def test_get_existing_terms_set_returns_lowercase_set(self, sample_node_id):
        """Should return lowercase set of existing term_original values for a node."""
        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.all.return_value = [("Revenue",), ("Employee",), ("EBITDA",)]

        service = DomainDictionaryService(mock_session, node_id=sample_node_id)
        result = service.get_existing_terms_set()

        assert isinstance(result, set)
        assert result == {"revenue", "employee", "ebitda"}

    def test_get_existing_terms_set_empty(self, sample_node_id):
        """Should return empty set when the node has no terms."""
        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.all.return_value = []

        service = DomainDictionaryService(mock_session, node_id=sample_node_id)
        result = service.get_existing_terms_set()

        assert result == set()

    def test_get_existing_terms_set_requires_node_id(self):
        """Calling without node_id (neither arg nor service-level) must error."""
        mock_session = MagicMock()
        service = DomainDictionaryService(mock_session)

        with pytest.raises(ValueError, match="node_id"):
            service.get_existing_terms_set()

    def test_get_existing_terms_set_argument_overrides_service_level(self):
        """An explicit node_id argument should win over self.node_id."""
        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.all.return_value = []

        service_node = str(uuid4())
        arg_node = str(uuid4())
        service = DomainDictionaryService(mock_session, node_id=service_node)
        service.get_existing_terms_set(node_id=arg_node)

        # filter() was invoked with the argument-level node, not the service-level one
        assert mock_query.filter.called
        filter_arg = mock_query.filter.call_args[0][0]
        bind_value = filter_arg.right.value
        assert str(bind_value) == arg_node
        assert str(bind_value) != service_node

    def test_add_terms_batch_uses_composite_conflict_target(self, sample_node_id):
        """Batch insert must target the composite (term_original, node_id) constraint."""
        mock_session = MagicMock()
        mock_result = MagicMock()
        mock_result.rowcount = 2
        mock_session.execute.return_value = mock_result
        service = DomainDictionaryService(mock_session)

        entries = [
            DomainTermDictionary(
                id=uuid4(),
                term_original="revenue",
                source_language="en",
                domain_category="finance",
                tenant_id=uuid4(),
                node_id=sample_node_id,
            ),
            DomainTermDictionary(
                id=uuid4(),
                term_original="employee",
                source_language="en",
                domain_category="hr",
                tenant_id=uuid4(),
                node_id=sample_node_id,
            ),
        ]

        result = service.add_terms_batch(entries)

        mock_session.execute.assert_called_once()
        mock_session.commit.assert_called_once()
        stmt = mock_session.execute.call_args[0][0]
        compiled = str(stmt.compile(dialect=postgresql.dialect()))
        assert "ON CONFLICT" in compiled.upper()
        assert "term_original" in compiled
        assert "node_id" in compiled
        assert "confidence" in compiled
        assert result == 2

    def test_add_terms_batch_empty_is_noop(self):
        """Empty entries list should return 0 without touching the session."""
        mock_session = MagicMock()
        service = DomainDictionaryService(mock_session)

        result = service.add_terms_batch([])

        mock_session.execute.assert_not_called()
        mock_session.commit.assert_not_called()
        assert result == 0

    def test_same_term_different_nodes_both_allowed(self):
        """Per-node dedup: get_existing_terms_set for node A must not surface node B's terms.

        Simulates two sequential calls from different services; the filter clause
        on each query is the application-level enforcement of per-node scope.
        """
        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query

        node_a = str(uuid4())
        node_b = str(uuid4())

        # Node A has "revenue" only; node B has nothing.
        mock_query.all.side_effect = [[("revenue",)], []]

        service_a = DomainDictionaryService(mock_session, node_id=node_a)
        service_b = DomainDictionaryService(mock_session, node_id=node_b)

        terms_a = service_a.get_existing_terms_set()
        terms_b = service_b.get_existing_terms_set()

        assert terms_a == {"revenue"}
        assert terms_b == set()
        # Both calls applied a filter — no global scan.
        assert mock_query.filter.call_count == 2
