"""Feed engine service tests"""

import uuid
from unittest.mock import AsyncMock, MagicMock

import pytest
from sqlalchemy.ext.asyncio import AsyncSession


def _make_sub_row(user_id, tag_id, match_mode):
    """Create a mock SQLAlchemy Row that supports both attribute access and
    tuple unpacking, matching how the feed_engine iterates user_data."""
    row = MagicMock()
    row.user_id = user_id
    row.tag_id = tag_id
    row.match_mode = match_mode
    row.__iter__.return_value = iter([user_id, tag_id, match_mode])
    return row


def _make_count_row(user_id, total):
    """Mock row for user subscription count query."""
    row = MagicMock()
    row.user_id = user_id
    row.total = total
    row.__iter__.return_value = iter([user_id, total])
    return row


def _make_existing_row(user_id, literature_id):
    """Mock row for existing feed check query."""
    row = MagicMock()
    row.user_id = user_id
    row.literature_id = literature_id
    row.__iter__.return_value = iter([user_id, literature_id])
    return row


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_no_tags():
    """No tags for the literature -> returns 0 immediately"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([])
    db.execute.return_value = mock_tags

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 0
    assert db.execute.call_count == 1


@pytest.mark.asyncio
async def test_tags_no_subscriptions():
    """Tags exist but no active subscriptions -> returns 0"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    tag_id = uuid.uuid4()

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_id, True)])

    mock_subs = MagicMock()
    mock_subs.all.return_value = []

    db.execute.side_effect = [mock_tags, mock_subs]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 0
    assert db.execute.call_count == 2


@pytest.mark.asyncio
async def test_standard_mode_priority_must_read():
    """Standard mode, 3+ tags with major -> must_read priority"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid = uuid.uuid4()
    tag_ids = [uuid.uuid4() for _ in range(3)]

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_ids[0], True), (tag_ids[1], True), (tag_ids[2], True)])

    rows = [_make_sub_row(uid, tid, "standard") for tid in tag_ids]
    mock_subs = MagicMock()
    mock_subs.all.return_value = rows

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([_make_count_row(uid, 3)])

    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 1
    assert db.add.call_count == 1
    added_feed = db.add.call_args[0][0]
    assert added_feed.user_id == uid
    assert added_feed.literature_id == lit_id
    assert added_feed.priority == "must_read"


@pytest.mark.asyncio
async def test_standard_mode_priority_recommended():
    """Standard mode, 2 tags with 1 major -> recommended priority"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid = uuid.uuid4()
    tag_id_1 = uuid.uuid4()
    tag_id_2 = uuid.uuid4()

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_id_1, True), (tag_id_2, False)])

    rows = [
        _make_sub_row(uid, tag_id_1, "standard"),
        _make_sub_row(uid, tag_id_2, "standard"),
    ]
    mock_subs = MagicMock()
    mock_subs.all.return_value = rows

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([_make_count_row(uid, 2)])

    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 1
    added_feed = db.add.call_args[0][0]
    assert added_feed.priority == "recommended"


@pytest.mark.asyncio
async def test_standard_mode_priority_related():
    """Standard mode, single non-major tag -> related priority"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid = uuid.uuid4()
    tag_id = uuid.uuid4()

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_id, False)])

    mock_subs = MagicMock()
    mock_subs.all.return_value = [_make_sub_row(uid, tag_id, "standard")]

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([_make_count_row(uid, 1)])

    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 1
    added_feed = db.add.call_args[0][0]
    assert added_feed.priority == "related"


@pytest.mark.asyncio
async def test_loose_mode_must_read():
    """Loose mode, 3+ tags -> must_read priority"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid = uuid.uuid4()
    tag_ids = [uuid.uuid4() for _ in range(3)]

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_ids[0], False), (tag_ids[1], False), (tag_ids[2], True)])

    rows = [_make_sub_row(uid, tid, "loose") for tid in tag_ids]
    mock_subs = MagicMock()
    mock_subs.all.return_value = rows

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([_make_count_row(uid, 3)])

    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 1
    added_feed = db.add.call_args[0][0]
    assert added_feed.priority == "must_read"


@pytest.mark.asyncio
async def test_loose_mode_recommended():
    """Loose mode, 1 tag -> recommended priority"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid = uuid.uuid4()
    tag_id = uuid.uuid4()

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_id, True)])

    mock_subs = MagicMock()
    mock_subs.all.return_value = [_make_sub_row(uid, tag_id, "loose")]

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([_make_count_row(uid, 1)])

    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 1
    added_feed = db.add.call_args[0][0]
    assert added_feed.priority == "recommended"


@pytest.mark.asyncio
async def test_strict_mode_partial_match_skips():
    """Strict mode, not all subscribed tags match -> feed is skipped"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid = uuid.uuid4()
    tag_id_1 = uuid.uuid4()

    # Literature has only tag_id_1 (major)
    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_id_1, True)])

    # User has 1 matching subscription but 2 total active subscriptions
    mock_subs = MagicMock()
    mock_subs.all.return_value = [_make_sub_row(uid, tag_id_1, "strict")]

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([_make_count_row(uid, 2)])

    # existing_feeds query runs before the user loop, must be provided
    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 0
    assert db.add.call_count == 0


@pytest.mark.asyncio
async def test_strict_mode_all_match_creates_feed():
    """Strict mode, all subscribed tags match -> feed created with recommended priority"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid = uuid.uuid4()
    tag_ids = [uuid.uuid4() for _ in range(2)]

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_ids[0], True), (tag_ids[1], False)])

    rows = [_make_sub_row(uid, tid, "strict") for tid in tag_ids]
    mock_subs = MagicMock()
    mock_subs.all.return_value = rows

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([_make_count_row(uid, 2)])

    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 1
    added_feed = db.add.call_args[0][0]
    # strict fallback: major_cnt >= 2 -> must_read, else recommended
    # 1 major (tag_ids[0]) -> recommended
    assert added_feed.priority == "recommended"


@pytest.mark.asyncio
async def test_existing_feed_prevents_duplicate():
    """Feed already exists for (user, literature) -> skip duplicate"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid = uuid.uuid4()
    tag_id = uuid.uuid4()

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_id, True)])

    mock_subs = MagicMock()
    mock_subs.all.return_value = [_make_sub_row(uid, tag_id, "standard")]

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([_make_count_row(uid, 1)])

    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([_make_existing_row(uid, lit_id)])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 0
    assert db.add.call_count == 0


@pytest.mark.asyncio
async def test_multiple_users():
    """Multiple users subscribed -> feeds created for each"""
    db = AsyncMock(spec=AsyncSession)
    lit_id = uuid.uuid4()
    uid_1 = uuid.uuid4()
    uid_2 = uuid.uuid4()
    tag_id = uuid.uuid4()

    mock_tags = MagicMock()
    mock_tags.__iter__.return_value = iter([(tag_id, True)])

    mock_subs = MagicMock()
    mock_subs.all.return_value = [
        _make_sub_row(uid_1, tag_id, "standard"),
        _make_sub_row(uid_2, tag_id, "standard"),
    ]

    mock_total = MagicMock()
    mock_total.__iter__.return_value = iter([
        _make_count_row(uid_1, 1),
        _make_count_row(uid_2, 1),
    ])

    mock_existing = MagicMock()
    mock_existing.__iter__.return_value = iter([])

    db.execute.side_effect = [mock_tags, mock_subs, mock_total, mock_existing]

    from app.services.feed_engine import generate_feeds_for_literature

    result = await generate_feeds_for_literature(db, lit_id)

    assert result == 2
    assert db.add.call_count == 2


__all__ = []
