"""Tests for domain models, schemas, and validators (article & entity)."""
from __future__ import annotations

from datetime import datetime

import pytest
from pydantic import ValidationError as PydanticValidationError

from repo_core import ValidationError

# ---------------------------------------------------------------------------
# Article domain imports
# ---------------------------------------------------------------------------
from repo_core.domains.article import (
    Article,
    CreateArticleRequest,
    PatchArticleRequest,
    ArticleResponse,
    ArticleTitleValidator,
)
from repo_core.domains.article.validator import ArticleBodyValidator, build_article_pipeline

# ---------------------------------------------------------------------------
# Entity domain imports
# ---------------------------------------------------------------------------
from repo_core.domains.entity import (
    Entity,
    CreateEntityRequest,
    PatchEntityRequest,
    EntityResponse,
    EntityNameValidator,
)
from repo_core.domains.entity.validator import EntityIdValidator, build_entity_pipeline


# ===========================================================================
# Article — model
# ===========================================================================

def test_article_from_db_full():
    article = Article.from_db({"id": 1, "title": "Hello", "body": "World"})
    assert article.id == 1
    assert article.title == "Hello"
    assert article.body == "World"
    assert article.published is False
    assert article.created_at is None


def test_article_from_db_with_timestamps():
    ts = datetime(2024, 1, 1, 12, 0)
    article = Article.from_db({"id": 2, "title": "T", "created_at": ts, "updated_at": ts})
    assert article.created_at == ts
    assert article.updated_at == ts


def test_article_to_db_round_trip():
    article = Article(id=3, title="Round trip", body="content", published=True)
    row = article.to_db()
    restored = Article.from_db(row)
    assert restored == article


def test_article_to_db_partial_only_set_fields():
    article = Article.model_construct(id=1, title="New title")
    partial = article.to_db_partial()
    assert "id" in partial
    assert "title" in partial
    assert "body" not in partial
    assert "published" not in partial


# ===========================================================================
# Article — schemas
# ===========================================================================

def test_create_article_request_valid():
    req = CreateArticleRequest(title="Hello", body="World")
    assert req.title == "Hello"
    assert req.tags == []


def test_create_article_request_with_tags():
    req = CreateArticleRequest(title="Hello", body="World", tags=["news", "tech"])
    assert req.tags == ["news", "tech"]


def test_create_article_request_rejects_missing_title():
    with pytest.raises(PydanticValidationError):
        CreateArticleRequest(body="World")


def test_create_article_request_rejects_extra_fields():
    with pytest.raises(PydanticValidationError):
        CreateArticleRequest(title="T", body="B", rogue="x")


def test_patch_article_request_partial():
    patch = PatchArticleRequest(title="Updated")
    fields = patch.to_patch_dict()
    assert fields == {"title": "Updated"}
    assert "body" not in fields
    assert "published" not in fields


def test_patch_article_request_empty():
    patch = PatchArticleRequest()
    assert patch.to_patch_dict() == {}


def test_patch_article_request_published_only():
    patch = PatchArticleRequest(published=True)
    assert patch.to_patch_dict() == {"published": True}


def test_article_response_from_dict():
    resp = ArticleResponse.model_validate(
        {"id": 1, "title": "Hello", "body": "World", "published": False}
    )
    assert resp.id == 1
    assert resp.to_dict() == {
        "id": 1,
        "title": "Hello",
        "body": "World",
        "published": False,
        "created_at": None,
        "updated_at": None,
    }


def test_article_response_from_orm_object():
    class OrmArticle:
        id = 5
        title = "ORM"
        body = "content"
        published = True
        created_at = None
        updated_at = None

    resp = ArticleResponse.model_validate(OrmArticle())
    assert resp.id == 5
    assert resp.published is True


# ===========================================================================
# Article — validators
# ===========================================================================

async def test_article_title_validator_strips_whitespace():
    v = ArticleTitleValidator()
    result = await v.validate("  Hello  ")
    assert result == "Hello"


async def test_article_title_validator_rejects_blank():
    v = ArticleTitleValidator()
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("   ")
    assert exc_info.value.field == "title"


async def test_article_title_validator_rejects_too_long():
    v = ArticleTitleValidator(max_length=5)
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("Too long title")
    assert exc_info.value.field == "title"


async def test_article_title_validator_passes_at_limit():
    v = ArticleTitleValidator(max_length=5)
    result = await v.validate("Hello")
    assert result == "Hello"


async def test_article_body_validator_rejects_empty():
    v = ArticleBodyValidator()
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("   ")
    assert exc_info.value.field == "body"


async def test_article_body_validator_strips_and_passes():
    v = ArticleBodyValidator()
    result = await v.validate("  content  ")
    assert result == "content"


async def test_build_article_pipeline_validates_title():
    pipeline = build_article_pipeline()
    with pytest.raises(ValidationError):
        await pipeline.validate("")


async def test_build_article_pipeline_passes_good_title():
    pipeline = build_article_pipeline()
    result = await pipeline.validate("  My Title  ")
    assert result == "My Title"


# ===========================================================================
# Entity — model
# ===========================================================================

def test_entity_from_db_full():
    entity = Entity.from_db({"id": "abc-123", "name": "Acme Corp"})
    assert entity.id == "abc-123"
    assert entity.name == "Acme Corp"
    assert entity.description == ""
    assert entity.active is True


def test_entity_to_db_round_trip():
    entity = Entity(id="e1", name="Test", description="desc", active=False)
    row = entity.to_db()
    restored = Entity.from_db(row)
    assert restored == entity


def test_entity_to_db_partial():
    entity = Entity.model_construct(id="e1", name="Updated")
    partial = entity.to_db_partial()
    assert "id" in partial
    assert "name" in partial
    assert "description" not in partial


# ===========================================================================
# Entity — schemas
# ===========================================================================

def test_create_entity_request_valid():
    req = CreateEntityRequest(name="Acme Corp")
    assert req.name == "Acme Corp"
    assert req.description == ""
    assert req.active is True


def test_create_entity_request_all_fields():
    req = CreateEntityRequest(name="Corp", description="A company", active=False)
    assert req.active is False


def test_create_entity_request_rejects_missing_name():
    with pytest.raises(PydanticValidationError):
        CreateEntityRequest()


def test_patch_entity_request_partial():
    patch = PatchEntityRequest(active=False)
    fields = patch.to_patch_dict()
    assert fields == {"active": False}
    assert "name" not in fields


def test_patch_entity_request_empty():
    patch = PatchEntityRequest()
    assert patch.to_patch_dict() == {}


def test_entity_response_from_dict():
    resp = EntityResponse.model_validate(
        {"id": "e1", "name": "Acme", "description": "", "active": True}
    )
    assert resp.id == "e1"
    data = resp.to_dict()
    assert data["name"] == "Acme"
    assert data["active"] is True


# ===========================================================================
# Entity — validators
# ===========================================================================

async def test_entity_name_validator_strips_whitespace():
    v = EntityNameValidator()
    result = await v.validate("  Acme Corp  ")
    assert result == "Acme Corp"


async def test_entity_name_validator_rejects_blank():
    v = EntityNameValidator()
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("  ")
    assert exc_info.value.field == "name"


async def test_entity_name_validator_rejects_too_long():
    v = EntityNameValidator(max_length=5)
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("TooLongName")
    assert exc_info.value.field == "name"


async def test_entity_id_validator_passes():
    v = EntityIdValidator()
    result = await v.validate("abc-123")
    assert result == "abc-123"


async def test_entity_id_validator_strips():
    v = EntityIdValidator()
    result = await v.validate("  abc  ")
    assert result == "abc"


async def test_entity_id_validator_rejects_empty():
    v = EntityIdValidator()
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("")
    assert exc_info.value.field == "id"


async def test_build_entity_pipeline_validates_name():
    pipeline = build_entity_pipeline()
    with pytest.raises(ValidationError):
        await pipeline.validate("")


async def test_build_entity_pipeline_passes_good_name():
    pipeline = build_entity_pipeline()
    result = await pipeline.validate("  Acme Corp  ")
    assert result == "Acme Corp"
