import sys
import unittest
from pathlib import Path
from unittest.mock import patch


_HERE = Path(__file__).parent
sys.path.insert(0, str(_HERE))

import execute_creation_once  # noqa: E402


class _Strategy:
    def __init__(self, material_source):
        self.material_source = material_source
        self.use_ai_generated = material_source == "ai_generated"
        self.ai_fallback_to_history = False


class LandingVideoDedupeTest(unittest.TestCase):
    def test_same_crowd_package_excludes_landing_video_across_accounts_in_one_run(self):
        calls = []

        def fake_prepare(account_id, adgroup_id, *, excluded_material_ids, excluded_landing_ids, **kwargs):
            calls.append((account_id, adgroup_id, set(excluded_landing_ids)))
            return {
                "account_id": account_id,
                "adgroup_id": adgroup_id,
                "audience_tier": "wx*商业",
                "landing_video_id": 1000 + account_id,
                "_material_id": f"m-{account_id}",
                "material_source": "history",
            }

        with patch.object(execute_creation_once, "get_creation_account_ids", return_value=[1, 2]), \
            patch.object(execute_creation_once, "load_recoverable_prepared_records", return_value=[]), \
            patch.object(execute_creation_once, "get_account_crowd_package", return_value="wx*商业"), \
            patch.object(execute_creation_once, "load_recent_landing_usage_counts", return_value={"history": {}, "ai_generated": {}}), \
            patch.object(execute_creation_once, "load_account_material_strategy", return_value=_Strategy("history")), \
            patch.object(execute_creation_once, "find_ads_needing_creatives", side_effect=[
                [{"adgroup_id": 101, "creative_count": 3}],
                [{"adgroup_id": 201, "creative_count": 3}],
            ]), \
            patch.object(execute_creation_once, "build_landing_candidate_pool", return_value=object()), \
            patch.object(execute_creation_once, "prepare_one_creative_for_ad", side_effect=fake_prepare), \
            patch.object(execute_creation_once, "record_prepared_material_usage"):
            records = execute_creation_once.phase1_prepare(target_creatives=4)

        self.assertEqual(2, len(records))
        self.assertEqual(set(), calls[0][2])
        self.assertEqual({1001}, calls[1][2])

    def test_ai_landing_dedupe_is_independent_from_history_and_limits_once_per_ai_pool(self):
        calls = []

        def fake_prepare(account_id, adgroup_id, *, excluded_material_ids, excluded_landing_ids, **kwargs):
            calls.append(set(excluded_landing_ids))
            return {
                "account_id": account_id,
                "adgroup_id": adgroup_id,
                "audience_tier": "wx*商业",
                "landing_video_id": 777,
                "_material_id": f"ai:{account_id}-{adgroup_id}-{len(calls)}",
                "material_source": "ai_generated",
            }

        with patch.object(execute_creation_once, "get_creation_account_ids", return_value=[1, 2]), \
            patch.object(execute_creation_once, "load_recoverable_prepared_records", return_value=[]), \
            patch.object(execute_creation_once, "get_account_crowd_package", return_value="wx*商业"), \
            patch.object(
                execute_creation_once,
                "load_recent_landing_usage_counts",
                return_value={"history": {777: 1}, "ai_generated": {}},
            ), \
            patch.object(execute_creation_once, "load_account_material_strategy", return_value=_Strategy("ai_generated")), \
            patch.object(execute_creation_once, "find_ads_needing_creatives", side_effect=[
                [{"adgroup_id": 101, "creative_count": 11}],
                [{"adgroup_id": 201, "creative_count": 11}],
            ]), \
            patch.object(execute_creation_once, "build_landing_candidate_pool", return_value=object()), \
            patch.object(execute_creation_once, "prepare_one_creative_for_ad", side_effect=fake_prepare), \
            patch.object(execute_creation_once, "record_prepared_material_usage"):
            records = execute_creation_once.phase1_prepare(target_creatives=12)

        self.assertEqual(1, len(records))
        self.assertEqual(set(), calls[0])
        self.assertEqual({777}, calls[1])

    def test_phase1_reuses_landing_candidate_pool_for_multiple_creative_attempts(self):
        pool = object()
        build_calls = []
        prepare_calls = []

        def fake_build_pool(account_id):
            build_calls.append(account_id)
            return pool

        def fake_prepare(
            account_id,
            adgroup_id,
            *,
            excluded_material_ids,
            excluded_landing_ids,
            landing_candidates,
            failed_landing_ids,
        ):
            prepare_calls.append((landing_candidates, failed_landing_ids))
            return {
                "account_id": account_id,
                "adgroup_id": adgroup_id,
                "audience_tier": "R330",
                "landing_video_id": 1000 + len(prepare_calls),
                "_material_id": f"ai:{len(prepare_calls)}",
                "material_source": "ai_generated",
            }

        with patch.object(execute_creation_once, "get_creation_account_ids", return_value=[1]), \
            patch.object(execute_creation_once, "load_recoverable_prepared_records", return_value=[]), \
            patch.object(execute_creation_once, "get_account_crowd_package", return_value="R330"), \
            patch.object(execute_creation_once, "load_recent_landing_usage_counts", return_value={"history": {}, "ai_generated": {}}), \
            patch.object(execute_creation_once, "load_account_material_strategy", return_value=_Strategy("ai_generated")), \
            patch.object(execute_creation_once, "find_ads_needing_creatives", return_value=[
                {"adgroup_id": 101, "creative_count": 0},
            ]), \
            patch.object(execute_creation_once, "build_landing_candidate_pool", side_effect=fake_build_pool), \
            patch.object(execute_creation_once, "prepare_one_creative_for_ad", side_effect=fake_prepare), \
            patch.object(execute_creation_once, "record_prepared_material_usage"):
            records = execute_creation_once.phase1_prepare(target_creatives=2)

        self.assertEqual(2, len(records))
        self.assertEqual([1], build_calls)
        self.assertEqual([(pool, set()), (pool, set())], prepare_calls)

    def test_phase1_carries_failed_landing_ids_between_attempts(self):
        calls = []

        def fake_prepare(
            account_id,
            adgroup_id,
            *,
            excluded_material_ids,
            excluded_landing_ids,
            landing_candidates,
            failed_landing_ids,
        ):
            calls.append(set(failed_landing_ids))
            if len(calls) == 1:
                failed_landing_ids.add(777)
                return None
            return {
                "account_id": account_id,
                "adgroup_id": adgroup_id,
                "audience_tier": "R330",
                "landing_video_id": 888,
                "_material_id": "ai:ok",
                "material_source": "ai_generated",
            }

        with patch.object(execute_creation_once, "get_creation_account_ids", return_value=[1]), \
            patch.object(execute_creation_once, "load_recoverable_prepared_records", return_value=[]), \
            patch.object(execute_creation_once, "get_account_crowd_package", return_value="R330"), \
            patch.object(execute_creation_once, "load_recent_landing_usage_counts", return_value={"history": {}, "ai_generated": {}}), \
            patch.object(execute_creation_once, "load_account_material_strategy", return_value=_Strategy("ai_generated")), \
            patch.object(execute_creation_once, "find_ads_needing_creatives", return_value=[
                {"adgroup_id": 101, "creative_count": 0},
            ]), \
            patch.object(execute_creation_once, "build_landing_candidate_pool", return_value=object()), \
            patch.object(execute_creation_once, "prepare_one_creative_for_ad", side_effect=fake_prepare), \
            patch.object(execute_creation_once, "record_prepared_material_usage"):
            records = execute_creation_once.phase1_prepare(target_creatives=2)

        self.assertEqual(1, len(records))
        self.assertEqual([set(), {777}], calls)


if __name__ == "__main__":
    unittest.main()
