"""Tests for CriteriaService CRUD operations."""
import pytest

from crystallise.criteria.models import CriterionType, ExclusionCriterion, UndoAction
from crystallise.criteria.service import CriteriaService


@pytest.fixture
def service():
    return CriteriaService(project_id=1)


@pytest.fixture
def service_with_criterion(service):
    criterion, _ = service.add_criterion(
        category="Population",
        text="Adults aged 18 and over",
        description="Only adult subjects",
    )
    return service, criterion


class TestAddCriterion:
    def test_stores_criterion_and_returns_tuple(self, service):
        criterion, undo = service.add_criterion(
            category="Population",
            text="Adults 18+",
        )
        assert isinstance(criterion, ExclusionCriterion)
        assert isinstance(undo, UndoAction)
        assert criterion.id == 1
        assert criterion.category == "Population"
        assert criterion.text == "Adults 18+"
        assert undo.action_type == "add"

    def test_auto_increments_ids(self, service):
        c1, _ = service.add_criterion(category="A", text="First")
        c2, _ = service.add_criterion(category="B", text="Second")
        assert c1.id == 1
        assert c2.id == 2


class TestGetCriterion:
    def test_retrieves_by_id(self, service_with_criterion):
        service, criterion = service_with_criterion
        retrieved = service.get_criterion(criterion.id)
        assert retrieved is not None
        assert retrieved.id == criterion.id
        assert retrieved.text == criterion.text

    def test_returns_none_for_missing(self, service):
        assert service.get_criterion(999) is None


class TestGetAllCriteria:
    def test_returns_all_for_project(self, service):
        service.add_criterion(category="A", text="First")
        service.add_criterion(category="B", text="Second")
        service.add_criterion(category="C", text="Third")
        all_criteria = service.get_all_criteria()
        assert len(all_criteria) == 3

    def test_filter_by_type(self, service):
        service.add_criterion(category="A", text="Include this", criterion_type=CriterionType.include)
        service.add_criterion(category="B", text="Exclude this", criterion_type=CriterionType.exclude)
        includes = service.get_all_criteria(criterion_type=CriterionType.include)
        excludes = service.get_all_criteria(criterion_type=CriterionType.exclude)
        assert len(includes) == 1
        assert len(excludes) == 1


class TestUpdateCriterion:
    def test_modifies_fields(self, service_with_criterion):
        service, criterion = service_with_criterion
        updated, undo = service.update_criterion(
            criterion.id, {"text": "Updated text", "description": "New description"}
        )
        assert updated.text == "Updated text"
        assert updated.description == "New description"
        assert undo.action_type == "update"

    def test_raises_for_missing_criterion(self, service):
        with pytest.raises(ValueError, match="not found"):
            service.update_criterion(999, {"text": "New text"})

    def test_ignores_disallowed_fields(self, service_with_criterion):
        service, criterion = service_with_criterion
        updated, _ = service.update_criterion(
            criterion.id, {"id": 999, "project_id": 999, "text": "Valid update"}
        )
        assert updated.id == criterion.id
        assert updated.text == "Valid update"


class TestDeleteCriterion:
    def test_removes_criterion(self, service_with_criterion):
        service, criterion = service_with_criterion
        undo = service.delete_criterion(criterion.id)
        assert isinstance(undo, UndoAction)
        assert undo.action_type == "delete"
        assert service.get_criterion(criterion.id) is None

    def test_raises_for_missing_criterion(self, service):
        with pytest.raises(ValueError, match="not found"):
            service.delete_criterion(999)


class TestUndo:
    def test_undo_add(self, service):
        criterion, undo = service.add_criterion(category="A", text="Test")
        assert service.get_criterion(criterion.id) is not None
        service.undo(undo)
        assert service.get_criterion(criterion.id) is None

    def test_undo_delete(self, service_with_criterion):
        service, criterion = service_with_criterion
        undo = service.delete_criterion(criterion.id)
        assert service.get_criterion(criterion.id) is None
        service.undo(undo)
        restored = service.get_criterion(criterion.id)
        assert restored is not None
        assert restored.text == criterion.text

    def test_undo_update(self, service_with_criterion):
        service, criterion = service_with_criterion
        original_text = criterion.text
        _, undo = service.update_criterion(criterion.id, {"text": "Changed text"})
        assert service.get_criterion(criterion.id).text == "Changed text"
        service.undo(undo)
        assert service.get_criterion(criterion.id).text == original_text
