from magento.media_cleanup import cleanup_orphaned_product_media


class _FakeApi:
    def __init__(self):
        self.deleted = []
        self.entries = [
            {"id": 11928, "file": "/i/m/image_0_1845.png", "position": 0, "types": ["image", "small_image", "thumbnail"]},
            {"id": 12709, "file": "/i/m/image_1_37.png", "position": 1, "types": []},
            {"id": 6393, "file": "/i/m/image_0_1234.jpg", "position": 0, "types": []},
            {"id": 6394, "file": "/i/m/image_2_33.jpg", "position": 2, "types": []},
            {"id": 6395, "file": "/i/m/image_3_33.jpg", "position": 3, "types": []},
        ]

    def get_product_media(self, sku):
        return 200, list(self.entries), None

    def delete_product_media(self, sku, entry_id):
        self.deleted.append((sku, entry_id))
        self.entries = [entry for entry in self.entries if int(entry["id"]) != int(entry_id)]
        return 200, None


class _FakeMediaRepo:
    def __init__(self):
        self.rows = [
            {
                "id": 1,
                "sku": "ACH-BPP09",
                "remote_url": "https://cdn/close.png",
                "magento_entry_id": 11928,
                "magento_file": "/i/m/image_0_1845.png",
            },
            {
                "id": 2,
                "sku": "ACH-BPP09",
                "remote_url": "https://cdn/open.png",
                "magento_entry_id": 12709,
                "magento_file": "/i/m/image_1_37.png",
            },
            {
                "id": 3,
                "sku": "ACH-BPP09",
                "remote_url": "https://cdn/v1a.jpg",
                "magento_entry_id": 3662,
                "magento_file": "/i/m/image_0_112.jpg",
            },
            {
                "id": 4,
                "sku": "ACH-BPP09",
                "remote_url": "https://cdn/v1b.jpg",
                "magento_entry_id": 6394,
                "magento_file": "/i/m/image_2_33.jpg",
            },
            {
                "id": 5,
                "sku": "ACH-BPP09",
                "remote_url": "https://cdn/v2.jpg",
                "magento_entry_id": 6395,
                "magento_file": "/i/m/image_3_33.jpg",
            },
            {
                "id": 6,
                "sku": "ACH-BPP09",
                "remote_url": "https://cdn/door.jpg",
                "magento_entry_id": 6393,
                "magento_file": "/i/m/image_0_1234.jpg",
            },
        ]
        self.deleted_urls = []

    def list_rows_for_sku(self, connection_id, sku):
        return list(self.rows)

    def delete_for_sku_urls(self, connection_id, sku, remote_urls):
        wanted = set(remote_urls)
        before = len(self.rows)
        self.rows = [row for row in self.rows if row["remote_url"] not in wanted]
        self.deleted_urls.extend(sorted(wanted))
        return before - len(self.rows)


class _FakeCatalogRepo:
    def __init__(self):
        self.calls = []

    def upsert_catalog_state(self, connection_id, sku, **kwargs):
        self.calls.append((connection_id, sku, kwargs))


def test_cleanup_orphaned_product_media_deletes_stale_map_entries_and_refreshes_state():
    api = _FakeApi()
    media_repo = _FakeMediaRepo()
    catalog_repo = _FakeCatalogRepo()

    result = cleanup_orphaned_product_media(
        connection_id=1,
        sku="ACH-BPP09",
        desired_urls=[
            "https://cdn/close.png",
            "https://cdn/open.png",
            "https://cdn/v1a.jpg",
            "https://cdn/v1b.jpg",
            "https://cdn/v2.jpg",
        ],
        api=api,
        media_repo=media_repo,
        catalog_repo=catalog_repo,
        dry_run=False,
    )

    assert result["status"] == "ok"
    assert api.deleted == [("ACH-BPP09", 6393)]
    assert media_repo.deleted_urls == ["https://cdn/door.jpg"]
    assert result["removed_map_rows"] == 1
    assert result["media_entry_count_before"] == 5
    assert result["media_entry_count_after"] == 4
    assert catalog_repo.calls


def test_cleanup_orphaned_product_media_dry_run_reports_targets_without_deleting():
    api = _FakeApi()
    media_repo = _FakeMediaRepo()
    catalog_repo = _FakeCatalogRepo()

    result = cleanup_orphaned_product_media(
        connection_id=1,
        sku="ACH-BPP09",
        desired_urls=[
            "https://cdn/close.png",
            "https://cdn/open.png",
            "https://cdn/v1a.jpg",
            "https://cdn/v1b.jpg",
            "https://cdn/v2.jpg",
        ],
        api=api,
        media_repo=media_repo,
        catalog_repo=catalog_repo,
        dry_run=True,
    )

    assert result["status"] == "dry_run"
    assert result["delete_targets"][0]["remote_url"] == "https://cdn/door.jpg"
    assert api.deleted == []
    assert media_repo.deleted_urls == []
    assert catalog_repo.calls == []


def test_cleanup_orphaned_product_media_reports_upstream_status_on_initial_lookup_failure():
    class _FailingApi:
        def get_product_media(self, sku):
            return 401, [], "Unauthorized"

    result = cleanup_orphaned_product_media(
        connection_id=4,
        sku="ACH-BPP09",
        desired_urls=["https://cdn/close.png"],
        api=_FailingApi(),
        media_repo=_FakeMediaRepo(),
        catalog_repo=_FakeCatalogRepo(),
        dry_run=True,
    )

    assert result["status"] == "dry_run"
    assert result["upstream_status"] == 401
    assert result["error"] == "Unauthorized"
    assert any(target["entry_id"] == 6393 for target in result["delete_targets"])


def test_cleanup_purge_unmapped_deletes_live_entries_not_mapped_to_desired():
    api = _FakeApi()
    media_repo = _FakeMediaRepo()
    # Keep only close+open mapped; leave door/v1a/v1b/v2 maps but mark desired as close+open only.
    # Also leave an unmapped live gallery entry (6393 is mapped to door which becomes stale).
    catalog_repo = _FakeCatalogRepo()

    result = cleanup_orphaned_product_media(
        connection_id=1,
        sku="ACH-BPP09",
        desired_urls=["https://cdn/close.png", "https://cdn/open.png"],
        api=api,
        media_repo=media_repo,
        catalog_repo=catalog_repo,
        dry_run=False,
        purge_unmapped=True,
    )

    assert result["status"] == "ok"
    assert result["purge_unmapped"] is True
    assert sorted(result["keep_entry_ids"]) == [11928, 12709]
    deleted_ids = sorted(entry_id for _, entry_id in api.deleted)
    # stale mapped (door/v1b/v2) + any remaining unmapped live ids
    assert 6393 in deleted_ids
    assert 6394 in deleted_ids
    assert 6395 in deleted_ids
    assert 11928 not in deleted_ids
    assert 12709 not in deleted_ids
    assert set(api.deleted)  # something deleted
    assert catalog_repo.calls


def test_cleanup_purge_unmapped_uses_product_payload_when_media_endpoint_broken():
    class _BrokenMediaApi:
        def __init__(self):
            self.deleted = []

        def get_product_media(self, sku):
            return (
                400,
                [],
                '{"message":"The contents from the \\"%1\\" file can\'t be read. %2"}',
            )

        def get_product(self, sku):
            return {
                "sku": sku,
                "media_gallery_entries": [
                    {"id": 11928, "file": "/i/m/image_0_1845.png", "position": 0},
                    {"id": 12709, "file": "/i/m/image_1_37.png", "position": 1},
                    {"id": 99901, "file": "/i/m/image_0_3776.png", "position": 2},
                    {"id": 99902, "file": "/i/m/image_0_4671.jpg", "position": 3},
                ],
            }

        def delete_product_media(self, sku, entry_id):
            self.deleted.append((sku, entry_id))
            return 200, None

    api = _BrokenMediaApi()
    media_repo = _FakeMediaRepo()
    # Only close+open are desired; their mapped entry ids are 11928/12709.
    media_repo.rows = [
        row for row in media_repo.rows if row["remote_url"] in {"https://cdn/close.png", "https://cdn/open.png"}
    ]
    catalog_repo = _FakeCatalogRepo()

    result = cleanup_orphaned_product_media(
        connection_id=1,
        sku="ACH-BPP09",
        desired_urls=["https://cdn/close.png", "https://cdn/open.png"],
        api=api,
        media_repo=media_repo,
        catalog_repo=catalog_repo,
        dry_run=True,
        purge_unmapped=True,
    )

    assert result["status"] == "dry_run"
    assert result["media_source"] == "product_payload"
    assert sorted(result["keep_entry_ids"]) == [11928, 12709]
    delete_ids = sorted(t["entry_id"] for t in result["delete_targets"])
    assert delete_ids == [99901, 99902]
    assert api.deleted == []


def test_wipe_product_media_deletes_all_entries_and_map_rows():
    from magento.media_cleanup import wipe_product_media

    class _Api:
        def __init__(self):
            self.deleted = []
            self.entries = [
                {"id": 1, "file": "/i/m/a.jpg"},
                {"id": 2, "file": "/i/m/b.jpg"},
            ]

        def get_product_media(self, sku):
            return 200, list(self.entries), None

        def delete_product_media(self, sku, entry_id):
            self.deleted.append(entry_id)
            self.entries = [e for e in self.entries if int(e["id"]) != int(entry_id)]
            return 200, None

    class _Repo(_FakeMediaRepo):
        def delete_all_for_sku(self, connection_id, sku):
            n = len(self.rows)
            self.rows = []
            return n

    api = _Api()
    media_repo = _Repo()
    catalog_repo = _FakeCatalogRepo()
    result = wipe_product_media(
        connection_id=1,
        sku="ASW-BEC1224L",
        api=api,
        media_repo=media_repo,
        catalog_repo=catalog_repo,
        dry_run=False,
    )
    assert result["status"] == "ok"
    assert result["wipe_all"] is True
    assert sorted(api.deleted) == [1, 2]
    assert result["removed_map_rows"] == 6
    assert result["media_entry_count_after"] == 0
    assert media_repo.rows == []


def test_cleanup_orphaned_product_media_fallback_deletes_by_map_entry_id_when_gallery_read_is_broken():
    class _BrokenReadApi:
        def __init__(self):
            self.deleted = []
            self.calls = 0

        def get_product_media(self, sku):
            self.calls += 1
            if self.calls == 1:
                return 400, [], "broken file on disk"
            return 200, [{"id": 11928, "file": "/i/m/image_0_1845.png"}], None

        def delete_product_media(self, sku, entry_id):
            self.deleted.append((sku, entry_id))
            return 200, None

    api = _BrokenReadApi()
    media_repo = _FakeMediaRepo()
    catalog_repo = _FakeCatalogRepo()

    result = cleanup_orphaned_product_media(
        connection_id=4,
        sku="ACH-BPP09",
        desired_urls=[
            "https://cdn/close.png",
            "https://cdn/open.png",
            "https://cdn/v1a.jpg",
            "https://cdn/v1b.jpg",
            "https://cdn/v2.jpg",
        ],
        api=api,
        media_repo=media_repo,
        catalog_repo=catalog_repo,
        dry_run=False,
    )

    assert result["status"] == "ok"
    assert api.deleted == [("ACH-BPP09", 6393)]
    assert media_repo.deleted_urls == ["https://cdn/door.jpg"]
    assert result["removed_map_rows"] == 1
    assert catalog_repo.calls


def test_cleanup_orphaned_media_batch_processes_in_chunks_and_skips_empty_master():
    from unittest.mock import MagicMock, patch

    from magento.media_cleanup import cleanup_orphaned_media_batch

    api = _FakeApi()
    media_repo = _FakeMediaRepo()
    catalog_repo = _FakeCatalogRepo()
    session = MagicMock()
    progress = []

    def _desired(_session, sku):
        if sku == "SKIP-ME":
            return []
        return ["https://cdn/close.png", "https://cdn/open.png"]

    with patch("magento.media_cleanup.desired_image_urls_for_sku", side_effect=_desired):
        with patch("magento.media_cleanup.cleanup_orphaned_product_media") as cleanup:
            cleanup.return_value = {
                "status": "ok",
                "delete_targets": [{"entry_id": 1}],
                "deleted_entries": [{"entry_id": 1, "remote_url": "https://cdn/door.jpg"}],
                "removed_map_rows": 1,
            }
            result = cleanup_orphaned_media_batch(
                connection_id=1,
                skus=["ACH-BPP09", "SKIP-ME", "ACH-BPP09", "OTHER"],
                api=api,
                media_repo=media_repo,
                catalog_repo=catalog_repo,
                session=session,
                dry_run=False,
                purge_unmapped=True,
                batch_size=1,
                on_progress=progress.append,
                include_sku_results=True,
            )

    assert result["status"] == "ok"
    assert result["mode"] == "orphans_only"
    assert result["sku_count"] == 3  # deduped
    assert result["ok_count"] == 2
    assert result["skipped_count"] == 1
    assert result["deleted_entry_count"] == 2
    assert cleanup.call_count == 2
    assert all(call.kwargs["purge_unmapped"] is True for call in cleanup.call_args_list)
    assert session.commit.call_count == 3  # one commit per batch of size 1
    assert progress
    assert progress[-1]["stage"] == "done"


def test_magento_media_cleanup_channel_job_runs_batch():
    from unittest.mock import MagicMock, patch

    from app.jobs.channel_jobs import _execute_channel_job

    session = MagicMock()
    job = MagicMock()
    job.id = 44
    job.channel_type = "magento"
    job.job_type = "media_cleanup"
    job.native_connection_id = 1
    job.dry_run = True
    job.result = {
        "_options": {
            "all_assigned": True,
            "batch_size": 50,
            "purge_unmapped": True,
            "connection_id": 1,
        }
    }

    with patch("db.magento_repositories.SqlAlchemyMagentoConnectionRepository") as conn_cls:
        conn_cls.return_value.get_for_sync.return_value = {"id": 1}
        with patch("db.magento_repositories.SqlAlchemyMagentoMediaMapRepository"):
            with patch("db.magento_repositories.SqlAlchemyMagentoCatalogStateRepository"):
                with patch("magento.oauth_client.MagentoOAuthClient"):
                    with patch("magento.oauth_client.build_magento_oauth_kwargs", return_value={}):
                        with patch("magento.magento_api.MagentoRestClient"):
                            with patch(
                                "magento.media_cleanup.resolve_orphan_cleanup_skus",
                                return_value=["A", "B"],
                            ):
                                with patch(
                                    "magento.media_cleanup.cleanup_orphaned_media_batch",
                                    return_value={
                                        "status": "dry_run",
                                        "sku_count": 2,
                                        "ok_count": 2,
                                        "skipped_count": 0,
                                        "partial_count": 0,
                                        "failed_count": 0,
                                        "total_count": 2,
                                        "success_count": 2,
                                        "error_count": 0,
                                        "message": "ok",
                                    },
                                ) as batch:
                                    result = _execute_channel_job(session, job)

    assert result["backend"] == "magento_media_cleanup"
    assert result["status"] == "completed"
    assert result["cleanup_status"] == "dry_run"
    batch.assert_called_once()
    assert batch.call_args.kwargs["purge_unmapped"] is True
    assert batch.call_args.kwargs["batch_size"] == 50
    assert batch.call_args.kwargs["skus"] == ["A", "B"]
