import uuid from datetime import UTC, datetime, timedelta import pytest import pytest_asyncio from unittest.mock import patch, AsyncMock, MagicMock from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker from sqlalchemy.pool import StaticPool from app.database import Base from app.models.attribution_record import AttributionRecord from app.services.attribution.attribution_engine import AttributionEngine @pytest.fixture def engine(): return create_async_engine( "sqlite+aiosqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) @pytest_asyncio.fixture async def session(engine): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async with session_maker() as s: yield s async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() class TestAttributionWindowDefault: def test_default_window_days_is_28(self): with patch("app.services.attribution.attribution_engine.settings") as mock_settings: mock_settings.ATTRIBUTION_WINDOW_DAYS = 28 engine = AttributionEngine() assert engine.window_days == 28 @pytest.mark.asyncio async def test_start_tracking_uses_default_28_days(self, session): with patch("app.services.attribution.attribution_engine.settings") as mock_settings: mock_settings.ATTRIBUTION_WINDOW_DAYS = 28 eng = AttributionEngine() with patch.object(eng, "_get_latest_score", return_value=50.0): record = await eng.start_tracking( db=session, brand_id=uuid.uuid4(), content_id=uuid.uuid4(), user_id="test_user", ) assert record.attribution_window_days == 28 expected_end = record.published_at + timedelta(days=28) assert record.window_end_at is not None time_diff = abs((record.window_end_at - expected_end).total_seconds()) assert time_diff < 2 class TestAttributionWindowCustom: def test_custom_window_days_14(self): with patch("app.services.attribution.attribution_engine.settings") as mock_settings: mock_settings.ATTRIBUTION_WINDOW_DAYS = 14 eng = AttributionEngine() assert eng.window_days == 14 @pytest.mark.asyncio async def test_start_tracking_uses_custom_14_days(self, session): with patch("app.services.attribution.attribution_engine.settings") as mock_settings: mock_settings.ATTRIBUTION_WINDOW_DAYS = 14 eng = AttributionEngine() with patch.object(eng, "_get_latest_score", return_value=50.0): record = await eng.start_tracking( db=session, brand_id=uuid.uuid4(), content_id=uuid.uuid4(), user_id="test_user", ) assert record.attribution_window_days == 14 expected_end = record.published_at + timedelta(days=14) assert record.window_end_at is not None time_diff = abs((record.window_end_at - expected_end).total_seconds()) assert time_diff < 2 @pytest.mark.asyncio async def test_start_tracking_uses_custom_7_days(self, session): with patch("app.services.attribution.attribution_engine.settings") as mock_settings: mock_settings.ATTRIBUTION_WINDOW_DAYS = 7 eng = AttributionEngine() with patch.object(eng, "_get_latest_score", return_value=50.0): record = await eng.start_tracking( db=session, brand_id=uuid.uuid4(), content_id=uuid.uuid4(), user_id="test_user", ) assert record.attribution_window_days == 7 expected_end = record.published_at + timedelta(days=7) assert record.window_end_at is not None time_diff = abs((record.window_end_at - expected_end).total_seconds()) assert time_diff < 2 class TestAttributionWindowConfigDriven: def test_config_attribute_window_days_default(self): from app.config import Settings test_settings = Settings( JWT_SECRET="a" * 32, ATTRIBUTION_WINDOW_DAYS=28, ) assert test_settings.ATTRIBUTION_WINDOW_DAYS == 28 def test_config_attribute_window_days_custom(self): from app.config import Settings test_settings = Settings( JWT_SECRET="a" * 32, ATTRIBUTION_WINDOW_DAYS=14, ) assert test_settings.ATTRIBUTION_WINDOW_DAYS == 14 @pytest.mark.asyncio async def test_window_end_matches_window_days(self, session): with patch("app.services.attribution.attribution_engine.settings") as mock_settings: mock_settings.ATTRIBUTION_WINDOW_DAYS = 21 eng = AttributionEngine() with patch.object(eng, "_get_latest_score", return_value=50.0): record = await eng.start_tracking( db=session, brand_id=uuid.uuid4(), content_id=uuid.uuid4(), user_id="test_user", ) assert record.attribution_window_days == 21 delta = record.window_end_at - record.published_at assert delta.days == 21