"""Tests for the Follow domain — model, schemas, validators, and repository."""
from __future__ import annotations

from datetime import datetime

import pytest
from pydantic import ValidationError as PydanticValidationError

from repo_core import ValidationError
from repo_core.domains.follow import (
    Follow,
    FollowTargetType,
    CreateFollowRequest,
    PatchFollowRequest,
    FollowResponse,
    FollowTargetTypeValidator,
    TargetIdValidator,
    build_follow_pipeline,
    FollowRepository,
)


# ===========================================================================
# Model
# ===========================================================================

def test_follow_from_db_full():
    follow = Follow.from_db({
        "id": 1,
        "user_id": 42,
        "target_type": "story",
        "target_id": "story-abc",
    })
    assert follow.id == 1
    assert follow.user_id == 42
    assert follow.target_type == FollowTargetType.STORY
    assert follow.target_id == "story-abc"


def test_follow_from_db_with_timestamps():
    ts = datetime(2024, 6, 1, 12, 0)
    follow = Follow.from_db({
        "id": 2, "user_id": 7,
        "target_type": "event", "target_id": "e-1",
        "created_at": ts, "updated_at": ts,
    })
    assert follow.created_at == ts
    assert follow.updated_at == ts


def test_follow_from_db_applies_defaults():
    follow = Follow.from_db({
        "id": 3, "user_id": 1,
        "target_type": "person", "target_id": "p-99",
    })
    assert follow.created_at is None
    assert follow.updated_at is None


def test_follow_to_db_round_trip():
    follow = Follow(id=1, user_id=42,
                    target_type=FollowTargetType.STORY, target_id="s-1")
    row = follow.to_db()
    restored = Follow.from_db(row)
    assert restored == follow


def test_follow_to_db_partial():
    follow = Follow.model_construct(id=1, user_id=42)
    partial = follow.to_db_partial()
    assert "id" in partial
    assert "user_id" in partial
    assert "target_type" not in partial
    assert "target_id" not in partial


def test_follow_accepts_all_target_types():
    for t in FollowTargetType:
        follow = Follow(id=1, user_id=1, target_type=t, target_id="x")
        assert follow.target_type == t


def test_follow_rejects_invalid_target_type():
    with pytest.raises(PydanticValidationError):
        Follow(id=1, user_id=1, target_type="invalid", target_id="x")


# ===========================================================================
# Schemas — CreateFollowRequest
# ===========================================================================

def test_create_follow_request_valid():
    req = CreateFollowRequest(
        user_id=42, target_type="story", target_id="s-abc")
    assert req.user_id == 42
    assert req.target_type == FollowTargetType.STORY
    assert req.target_id == "s-abc"


def test_create_follow_request_rejects_zero_user_id():
    with pytest.raises(PydanticValidationError):
        CreateFollowRequest(user_id=0, target_type="story", target_id="x")


def test_create_follow_request_rejects_empty_target_id():
    with pytest.raises(PydanticValidationError):
        CreateFollowRequest(user_id=1, target_type="story", target_id="")


def test_create_follow_request_rejects_missing_user_id():
    with pytest.raises(PydanticValidationError):
        CreateFollowRequest(target_type="story", target_id="x")


def test_create_follow_request_rejects_extra_fields():
    with pytest.raises(PydanticValidationError):
        CreateFollowRequest(user_id=1, target_type="story",
                            target_id="x", rogue=True)


# ===========================================================================
# Schemas — PatchFollowRequest
# ===========================================================================

def test_patch_follow_request_partial():
    patch = PatchFollowRequest(target_id="new-id")
    fields = patch.to_patch_dict()
    assert fields == {"target_id": "new-id"}
    assert "target_type" not in fields


def test_patch_follow_request_empty():
    patch = PatchFollowRequest()
    assert patch.to_patch_dict() == {}


def test_patch_follow_request_rejects_extra_fields():
    with pytest.raises(PydanticValidationError):
        PatchFollowRequest(unknown="x")


# ===========================================================================
# Schemas — FollowResponse
# ===========================================================================

def test_follow_response_from_dict():
    resp = FollowResponse.model_validate({
        "id": 1, "user_id": 42,
        "target_type": "story", "target_id": "s-1",
    })
    assert resp.id == 1
    assert resp.target_type == FollowTargetType.STORY


def test_follow_response_to_dict():
    resp = FollowResponse(
        id=1, user_id=42, target_type=FollowTargetType.STORY, target_id="s-1")
    data = resp.to_dict()
    assert data["id"] == 1
    assert data["user_id"] == 42
    assert data["target_type"] == "story"
    assert data["target_id"] == "s-1"
    assert data["created_at"] is None
    assert data["updated_at"] is None


def test_follow_response_from_orm_object():
    class FakeORM:
        id = 5
        user_id = 99
        target_type = "entity"
        target_id = "ent-uuid"
        created_at = None
        updated_at = None

    resp = FollowResponse.model_validate(FakeORM())
    assert resp.id == 5
    assert resp.user_id == 99
    assert resp.target_type == FollowTargetType.ENTITY


# ===========================================================================
# Validators — FollowTargetTypeValidator
# ===========================================================================

async def test_target_type_validator_passes_valid_types():
    v = FollowTargetTypeValidator()
    for t in FollowTargetType:
        result = await v.validate(t.value)
        assert result == t.value


async def test_target_type_validator_strips_and_lowercases():
    v = FollowTargetTypeValidator()
    result = await v.validate("  STORY  ")
    assert result == "story"


async def test_target_type_validator_rejects_invalid():
    v = FollowTargetTypeValidator()
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("movie")
    assert exc_info.value.field == "target_type"


# ===========================================================================
# Validators — TargetIdValidator
# ===========================================================================

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


async def test_target_id_validator_rejects_blank():
    v = TargetIdValidator()
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("   ")
    assert exc_info.value.field == "target_id"


async def test_target_id_validator_rejects_too_long():
    v = TargetIdValidator(max_length=5)
    with pytest.raises(ValidationError) as exc_info:
        await v.validate("too-long-id")
    assert exc_info.value.field == "target_id"


async def test_target_id_validator_passes_at_limit():
    v = TargetIdValidator(max_length=5)
    result = await v.validate("12345")
    assert result == "12345"


# ===========================================================================
# Validators — Pipeline
# ===========================================================================

async def test_build_follow_pipeline_validates_type():
    pipeline = build_follow_pipeline()
    type_v, id_v = pipeline._validators

    clean_type = await type_v.validate("  PERSON  ")
    assert clean_type == "person"

    with pytest.raises(ValidationError):
        await type_v.validate("invalid")


async def test_build_follow_pipeline_validates_id():
    pipeline = build_follow_pipeline()
    _, id_v = pipeline._validators

    clean_id = await id_v.validate("  target-42  ")
    assert clean_id == "target-42"

    with pytest.raises(ValidationError):
        await id_v.validate("")


# ===========================================================================
# Repository
# ===========================================================================

class TestFollowRepository:
    """Tests for FollowRepository using mock QueryBuilder."""

    @pytest.fixture
    def repo(self, db_query):
        return FollowRepository(model=Follow, db=db_query)

    async def test_follow_creates_new(self, repo, db_query):
        db_query.client.find_one.return_value = None
        db_query.client.create.return_value = {
            "id": 1, "user_id": 42,
            "target_type": "story", "target_id": "s-1",
        }

        result = await repo.follow(user_id=42, target_type=FollowTargetType.STORY, target_id="s-1")

        assert result.id == 1
        assert result.user_id == 42
        assert result.target_type == FollowTargetType.STORY

    async def test_follow_idempotent_returns_existing(self, repo, db_query):
        db_query.client.find_one.return_value = {
            "id": 5, "user_id": 42,
            "target_type": "story", "target_id": "s-1",
        }

        result = await repo.follow(user_id=42, target_type=FollowTargetType.STORY, target_id="s-1")

        assert result.id == 5
        db_query.client.create.assert_not_called()

    async def test_unfollow_returns_true_when_deleted(self, repo, db_query):
        db_query.client.delete.return_value = 1

        result = await repo.unfollow(user_id=42, target_type=FollowTargetType.STORY, target_id="s-1")

        assert result is True

    async def test_unfollow_returns_false_when_not_found(self, repo, db_query):
        db_query.client.delete.return_value = 0

        result = await repo.unfollow(user_id=42, target_type=FollowTargetType.STORY, target_id="s-1")

        assert result is False

    async def test_is_following_true(self, repo, db_query):
        db_query.client.find_one.return_value = {
            "id": 1, "user_id": 42,
            "target_type": "story", "target_id": "s-1",
        }

        assert await repo.is_following(42, FollowTargetType.STORY, "s-1") is True

    async def test_is_following_false(self, repo, db_query):
        db_query.client.find_one.return_value = None

        assert await repo.is_following(42, FollowTargetType.STORY, "s-1") is False

    async def test_list_following_all(self, repo, db_query):
        db_query.client.find_many.return_value = [
            {"id": 1, "user_id": 42, "target_type": "story", "target_id": "s-1"},
            {"id": 2, "user_id": 42, "target_type": "event", "target_id": "e-1"},
        ]
        db_query.client.count.return_value = 2

        results = await repo.list_following(42)

        assert len(results) == 2
        assert all(f.user_id == 42 for f in results)

    async def test_list_following_filtered_by_type(self, repo, db_query):
        db_query.client.find_many.return_value = [
            {"id": 1, "user_id": 42, "target_type": "story", "target_id": "s-1"},
        ]
        db_query.client.count.return_value = 1

        results = await repo.list_following(42, target_type=FollowTargetType.STORY)

        assert len(results) == 1
        assert results[0].target_type == FollowTargetType.STORY

    async def test_list_followers(self, repo, db_query):
        db_query.client.find_many.return_value = [
            {"id": 1, "user_id": 10, "target_type": "story", "target_id": "s-1"},
            {"id": 2, "user_id": 20, "target_type": "story", "target_id": "s-1"},
        ]

        results = await repo.list_followers(FollowTargetType.STORY, "s-1")

        assert len(results) == 2
        assert all(f.target_id == "s-1" for f in results)

    async def test_count_followers(self, repo, db_query):
        db_query.client.count.return_value = 5

        count = await repo.count_followers(FollowTargetType.STORY, "s-1")

        assert count == 5
