"""Tests for DomainModel per-backend field filtering
(_db_include, _search_include, _warehouse_include)."""

from __future__ import annotations

import pytest
from pydantic import BaseModel

from repo_core import DomainModel


# ---------------------------------------------------------------------------
# Test models with different _include configurations
# ---------------------------------------------------------------------------

class AllFieldsModel(DomainModel):
    """No _include restrictions — all fields go everywhere."""
    id: int
    title: str
    body: str
    score: float


class FilteredModel(DomainModel):
    """Explicit _db_include / _search_include restrictions."""
    id: int
    title: str
    body: str
    score: float
    internal_note: str = ""

    _db_include = {"id", "title", "body", "score"}
    _search_include = {"id", "title"}
    _warehouse_include = None  # all fields


class WarehouseOnlyModel(DomainModel):
    """Only warehouse gets all fields; db and search restricted."""
    id: int
    name: str
    analytics_score: float
    secret_field: str = "redacted"

    _db_include = {"id", "name"}
    _search_include = {"id", "name"}
    _warehouse_include = {"id", "name", "analytics_score"}  # no secret_field


# ===========================================================================
# AllFieldsModel — everything passes through
# ===========================================================================

def test_all_fields_to_db():
    m = AllFieldsModel(id=1, title="T", body="B", score=9.5)
    row = m.to_db()
    assert row == {"id": 1, "title": "T", "body": "B", "score": 9.5}


def test_all_fields_to_search():
    m = AllFieldsModel(id=1, title="T", body="B", score=9.5)
    doc = m.to_search()
    assert doc == {"id": 1, "title": "T", "body": "B", "score": 9.5}


def test_all_fields_to_warehouse():
    m = AllFieldsModel(id=1, title="T", body="B", score=9.5)
    row = m.to_warehouse()
    assert row == {"id": 1, "title": "T", "body": "B", "score": 9.5}


def test_all_fields_to_db_partial():
    m = AllFieldsModel.model_construct(id=1, title="New")
    partial = m.to_db_partial()
    assert partial == {"id": 1, "title": "New"}


def test_all_fields_to_search_partial():
    m = AllFieldsModel.model_construct(id=1, title="New")
    partial = m.to_search_partial()
    assert partial == {"id": 1, "title": "New"}


def test_all_fields_to_warehouse_partial():
    m = AllFieldsModel.model_construct(id=1, title="New")
    partial = m.to_warehouse_partial()
    assert partial == {"id": 1, "title": "New"}


# ===========================================================================
# FilteredModel — restrictive _db_include / _search_include
# ===========================================================================

def test_filtered_to_db_excludes_search_only_fields():
    m = FilteredModel(id=1, title="T", body="B", score=10.0, internal_note="secret")
    row = m.to_db()
    assert "internal_note" not in row
    assert row == {"id": 1, "title": "T", "body": "B", "score": 10.0}


def test_filtered_to_search_only_includes_search_fields():
    m = FilteredModel(id=1, title="T", body="B", score=10.0, internal_note="secret")
    doc = m.to_search()
    assert doc == {"id": 1, "title": "T"}
    assert "body" not in doc
    assert "score" not in doc


def test_filtered_to_warehouse_includes_all():
    m = FilteredModel(id=1, title="T", body="B", score=10.0, internal_note="secret")
    row = m.to_warehouse()
    assert row == {"id": 1, "title": "T", "body": "B", "score": 10.0, "internal_note": "secret"}


def test_filtered_to_db_partial_respects_include():
    m = FilteredModel.model_construct(id=1, title="New", internal_note="changed")
    partial = m.to_db_partial()
    assert partial == {"id": 1, "title": "New"}
    assert "internal_note" not in partial


def test_filtered_to_search_partial_respects_include():
    m = FilteredModel.model_construct(id=1, title="Searchable", body="changed")
    partial = m.to_search_partial()
    assert partial == {"id": 1, "title": "Searchable"}
    assert "body" not in partial


# ===========================================================================
# WarehouseOnlyModel — selective warehouse
# ===========================================================================

def test_warehouse_only_to_db():
    m = WarehouseOnlyModel(id=1, name="Test", analytics_score=99.0, secret_field="sssh")
    row = m.to_db()
    assert row == {"id": 1, "name": "Test"}
    assert "analytics_score" not in row
    assert "secret_field" not in row


def test_warehouse_only_to_search():
    m = WarehouseOnlyModel(id=1, name="Test", analytics_score=99.0, secret_field="sssh")
    doc = m.to_search()
    assert doc == {"id": 1, "name": "Test"}


def test_warehouse_only_to_warehouse():
    m = WarehouseOnlyModel(id=1, name="Test", analytics_score=99.0, secret_field="sssh")
    row = m.to_warehouse()
    assert row == {"id": 1, "name": "Test", "analytics_score": 99.0}
    assert "secret_field" not in row


# ===========================================================================
# from_db / from_search / from_warehouse — all use model_validate (no filtering)
# ===========================================================================

def test_from_db_accepts_all_fields_even_when_filtered():
    """from_db uses model_validate — it accepts any field regardless of _db_include."""
    m = FilteredModel.from_db({"id": 1, "title": "T", "body": "B", "score": 5.0, "internal_note": "x"})
    assert m.internal_note == "x"


def test_from_search_accepts_all_fields():
    m = FilteredModel.from_search({"id": 1, "title": "T", "body": "B", "score": 5.0})
    assert m.score == 5.0


def test_from_warehouse_accepts_all_fields():
    m = WarehouseOnlyModel.from_warehouse(
        {"id": 1, "name": "T", "analytics_score": 50.0, "secret_field": "leaked"}
    )
    assert m.secret_field == "leaked"


# ===========================================================================
# Edge cases: None _includes
# ===========================================================================

def test_none_db_include_means_all_fields():
    class M(DomainModel):
        id: int
        a: str
        b: str
        _db_include = None

    m = M(id=1, a="x", b="y")
    assert m.to_db() == {"id": 1, "a": "x", "b": "y"}


def test_none_search_include_means_all_fields():
    class M(DomainModel):
        id: int
        a: str
        _search_include = None

    m = M(id=1, a="x")
    assert m.to_search() == {"id": 1, "a": "x"}
