from contextlib import contextmanager
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient

from db.collection_landing_push import (
    READINESS_LINKED,
    READINESS_PROVISIONABLE,
    SKIP_REASON_SHOPIFY_NOT_FOUND,
    TARGET_SOURCE_LISTING_PATH,
)

COLLECTION_ROW = {
    "path_slug": "kitchen-cabinets/anna-stone-gray",
    "category_l1": "Kitchen Cabinets",
    "collection": "Anna Stone Gray",
    "title": "Anna Stone Gray",
    "has_landing_content": True,
}


@pytest.fixture
def api_client():
    from main import app

    return TestClient(app)


@pytest.fixture
def db_enabled():
    cfg = MagicMock()
    cfg.enabled = True
    with patch("main.load_db_config", return_value=cfg):
        yield cfg


@contextmanager
def mock_db_session(session):
    @contextmanager
    def _get_session():
        yield session

    with patch("main.get_session", _get_session):
        yield


def test_channel_collections_bootstrap_dry_run(api_client, db_enabled):
    session = MagicMock()
    bootstrap_result = {
        "channel": "shopify",
        "dry_run": True,
        "planned": ["brand", "subtitle"],
        "created": [],
        "skipped": [],
        "errors": [],
    }
    with mock_db_session(session):
        with patch(
            "db.collection_landing_push.bootstrap_collection_landing_channel",
            return_value=bootstrap_result,
        ) as bootstrap:
            response = api_client.post(
                "/api/channel/collections/bootstrap",
                params={"channel": "shopify", "connection_id": 10001, "dry_run": True},
            )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "ok"
    assert body["planned"] == ["brand", "subtitle"]
    bootstrap.assert_called_once_with(
        session,
        channel_code="shopify",
        connection_id=10001,
        dry_run=True,
    )
    session.commit.assert_not_called()


def test_channel_collections_bootstrap_live_queues(api_client, db_enabled):
    session = MagicMock()
    job_data = {
        "id": 42,
        "job_type": "collection_bootstrap",
        "status": "queued",
        "terminal": False,
    }
    connection = {
        "id": 1_000_001,
        "native_id": 1,
        "channel_type": "magento",
        "channel_code": "default",
    }
    with mock_db_session(session):
        with patch("main._resolve_channel_compat_connection", return_value=connection):
            with patch(
                "app.jobs.channel_jobs.enqueue_collection_bootstrap_job",
                return_value=MagicMock(id=42),
            ) as enqueue:
                with patch("app.jobs.channel_jobs.job_to_dict", return_value=job_data):
                    with patch("app.jobs.channel_job_worker.run_one"):
                        response = api_client.post(
                            "/api/channel/collections/bootstrap",
                            params={"channel": "magento", "connection_id": 1, "dry_run": False},
                        )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "queued"
    assert body["job_id"] == 42
    assert body["poll_url"] == "/api/channel-jobs/42"
    enqueue.assert_called_once()
    session.commit.assert_called_once()


def test_channel_collections_push_live_queues(api_client, db_enabled):
    session = MagicMock()
    job_data = {
        "id": 99,
        "job_type": "collection_push",
        "status": "queued",
        "terminal": False,
    }
    connection = {
        "id": 1_000_001,
        "native_id": 10001,
        "channel_type": "shopify",
        "channel_code": "test-shop",
    }
    with mock_db_session(session):
        with patch("main._resolve_channel_compat_connection", return_value=connection):
            with patch(
                "app.jobs.channel_jobs.enqueue_collection_push_job",
                return_value=MagicMock(id=99),
            ) as enqueue:
                with patch("app.jobs.channel_jobs.job_to_dict", return_value=job_data):
                    with patch("app.jobs.channel_job_worker.run_one"):
                        response = api_client.post(
                            "/api/channel/collections/push",
                            json={
                                "channel": "shopify",
                                "connection_id": 10001,
                                "path_slug": COLLECTION_ROW["path_slug"],
                                "dry_run": False,
                                "provision_missing_remote": False,
                            },
                        )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "queued"
    assert body["job_id"] == 99
    enqueue.assert_called_once()
    assert enqueue.call_args.kwargs["bootstrap_first"] is True
    session.commit.assert_called_once()


def test_dashboard_collection_save_live_queues(api_client, db_enabled):
    job_id = "job-123"
    with mock_db_session(MagicMock()):
        with patch("app.jobs.master_taxonomy_enqueue.start_master_taxonomy_job", return_value=job_id) as enqueue:
            response = api_client.post(
                "/api/dashboard/collections",
                json={
                    "title": "Anna Stone Gray",
                    "collection": "Anna Stone Gray",
                    "category_l1": "Kitchen Cabinets",
                    "delegate": True,
                },
            )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "queued"
    assert body["job_id"] == job_id
    assert body["poll_url"] == f"/api/master-taxonomy/jobs/{job_id}"
    assert body["path_slug"] == COLLECTION_ROW["path_slug"]
    enqueue.assert_called_once_with(
        "collection_save",
        {
            "payload": {
                "title": "Anna Stone Gray",
                "collection": "Anna Stone Gray",
                "category_l1": "Kitchen Cabinets",
                "delegate": True,
            }
        },
    )


def test_channel_collections_push_provision_flag(api_client, db_enabled):
    session = MagicMock()
    push_result = {
        "channel_code": "shopify",
        "dry_run": True,
        "provision_missing_remote": True,
        "count": 1,
        "pushed": 0,
        "skipped": 1,
        "errors": [],
        "results": [
            {
                "status": "preview",
                "path_slug": COLLECTION_ROW["path_slug"],
                "would_provision": True,
                "proposed_remote_title": "Anna Stone Gray",
            }
        ],
    }
    with mock_db_session(session):
        with patch(
            "db.collection_landing_push.push_collection_landing_channel",
            return_value=push_result,
        ) as push:
            response = api_client.post(
                "/api/channel/collections/push",
                json={
                    "channel": "shopify",
                    "path_slug": COLLECTION_ROW["path_slug"],
                    "dry_run": True,
                    "provision_missing_remote": True,
                },
            )

    assert response.status_code == 200
    assert response.json()["results"][0]["would_provision"] is True
    assert push.call_args.kwargs["provision_missing_remote"] is True


def test_channel_collections_push_skips_without_provision(api_client, db_enabled):
    session = MagicMock()
    push_result = {
        "channel_code": "magento",
        "dry_run": False,
        "provision_missing_remote": False,
        "count": 1,
        "pushed": 0,
        "skipped": 1,
        "errors": [],
        "results": [
            {
                "status": "skipped",
                "path_slug": COLLECTION_ROW["path_slug"],
                "reason": "magento_category_not_found",
            }
        ],
    }
    with mock_db_session(session):
        with patch(
            "db.collection_landing_push.push_collection_landing_channel",
            return_value=push_result,
        ):
            response = api_client.post(
                "/api/channel/collections/push",
                json={
                    "channel": "magento",
                    "connection_id": 1,
                    "path_slug": COLLECTION_ROW["path_slug"],
                    "dry_run": True,
                },
            )

    assert response.status_code == 200
    assert response.json()["skipped"] == 1
    assert response.json()["results"][0]["reason"] == "magento_category_not_found"


def test_channel_collections_push_invalid_channel(api_client, db_enabled):
    with mock_db_session(MagicMock()):
        response = api_client.post(
            "/api/channel/collections/push",
            json={"channel": "ebay", "dry_run": True},
        )

    assert response.status_code == 400


def test_dashboard_collection_channel_targets(api_client, db_enabled):
    session = MagicMock()
    target_payload = {
        "path_slug": COLLECTION_ROW["path_slug"],
        "connection_id": 10001,
        "targets": [
            {
                "channel_code": "shopify",
                "connection_id": 10001,
                "path_slug": COLLECTION_ROW["path_slug"],
                "remote_id": "gid://shopify/Collection/123",
                "target_source": TARGET_SOURCE_LISTING_PATH,
                "remote_path": "anna-stone-gray",
                "linked": True,
                "can_provision": False,
                "readiness": READINESS_LINKED,
                "skip_reason": None,
                "placement": {
                    "remote_title": "Anna Stone Gray",
                    "remote_handle": "anna-stone-gray",
                    "breadcrumb": ["Anna Stone Gray"],
                    "remote_taxonomy_kind": "collection",
                },
            }
        ],
    }
    with mock_db_session(session):
        with patch(
            "db.collection_landing_pages.get_collection_landing_page",
            return_value=COLLECTION_ROW,
        ):
            with patch(
                "db.collection_landing_push.describe_collection_channel_targets",
                return_value=target_payload,
            ) as describe:
                response = api_client.get(
                    f"/api/dashboard/collections/{COLLECTION_ROW['path_slug']}/channel-targets",
                    params={"channel": "shopify", "connection_id": 10001},
                )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "ok"
    assert body["targets"][0]["readiness"] == READINESS_LINKED
    describe.assert_called_once_with(
        session,
        COLLECTION_ROW,
        channel_code="shopify",
        connection_id=10001,
    )


def test_dashboard_collection_channel_targets_not_found(api_client, db_enabled):
    session = MagicMock()
    with mock_db_session(session):
        with patch("db.collection_landing_pages.get_collection_landing_page", return_value=None):
            response = api_client.get(
                "/api/dashboard/collections/missing-collection/channel-targets",
                params={"channel": "shopify"},
            )

    assert response.status_code == 404


def test_push_end_to_end_provision_then_push(api_client, db_enabled):
    """Live push is queued; dry-run preview still exercises the service layer."""
    session = MagicMock()
    with mock_db_session(session):
        with patch(
            "db.collection_landing_push.get_collection_landing_page",
            return_value=COLLECTION_ROW,
        ):
            with patch(
                "db.collection_landing_push.resolve_shopify_collection_target",
                side_effect=[
                    {"remote_id": None, "source": None, "remote_path": "anna-stone-gray"},
                ],
            ):
                with patch(
                    "db.collection_landing_push.provision_shopify_collection_target",
                    return_value={
                        "remote_id": "gid://shopify/Collection/999",
                        "source": "provisioned",
                        "remote_path": "anna-stone-gray",
                        "provisioned": True,
                    },
                ) as provision:
                    with patch("db.collection_landing_push._get_shopify_connection") as get_conn:
                        get_conn.return_value = MagicMock(shop_code="test-shop")
                        with patch("shopify.connections.build_client", return_value=MagicMock()):
                            with patch(
                                "shopify.collection_landing_push.push_shopify_collection_landing",
                                return_value={"metafields": 5},
                            ) as push_fields:
                                response = api_client.post(
                                    "/api/channel/collections/push",
                                    json={
                                        "channel": "shopify",
                                        "connection_id": 10001,
                                        "path_slug": COLLECTION_ROW["path_slug"],
                                        "dry_run": True,
                                        "provision_missing_remote": True,
                                    },
                                )

    assert response.status_code == 200
    body = response.json()
    assert body["pushed"] == 0
    assert body["results"][0]["status"] == "preview"
    assert body["results"][0]["would_provision"] is True
    provision.assert_not_called()
    push_fields.assert_not_called()
    session.commit.assert_not_called()


def test_dashboard_collection_reconcile_preview(api_client, db_enabled):
    session = MagicMock()
    reconcile_result = {
        "path_slug": COLLECTION_ROW["path_slug"],
        "sku_count": 8,
        "would_upsert": 8,
        "removed_from_other_collection_paths": 0,
    }
    with mock_db_session(session):
        with patch("db.collection_landing_pages.get_collection_landing_page", return_value=COLLECTION_ROW):
            with patch(
                "db.collection_sku_mapping.reconcile_collection_sku_assignments",
                return_value=reconcile_result,
            ) as reconcile:
                response = api_client.post(
                    f"/api/dashboard/collections/{COLLECTION_ROW['path_slug']}/skus/reconcile",
                    json={"dry_run": True},
                )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "ok"
    assert body["sku_count"] == 8
    reconcile.assert_called_once()
    session.commit.assert_not_called()


def test_dashboard_collection_reconcile_live_queues(api_client, db_enabled):
    session = MagicMock()
    job_data = {"id": 55, "job_type": "collection_reconcile", "status": "queued", "terminal": False}
    connection = {"id": 1_000_001, "native_id": 1, "channel_type": "magento", "channel_code": "default"}
    with mock_db_session(session):
        with patch("db.collection_landing_pages.get_collection_landing_page", return_value=COLLECTION_ROW):
            with patch("main._primary_collection_job_connection", return_value=connection):
                with patch("main._resolve_native_connection_id", return_value=1):
                    with patch(
                        "app.jobs.channel_jobs.enqueue_collection_reconcile_job",
                        return_value=MagicMock(id=55),
                    ) as enqueue:
                        with patch("app.jobs.channel_jobs.job_to_dict", return_value=job_data):
                            with patch("app.jobs.channel_job_worker.run_one"):
                                response = api_client.post(
                                    f"/api/dashboard/collections/{COLLECTION_ROW['path_slug']}/skus/reconcile",
                                    json={"dry_run": False, "sync_magento": True, "magento_connection_id": 1},
                                )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "queued"
    assert body["job_id"] == 55
    assert body["poll_url"] == "/api/channel-jobs/55"
    enqueue.assert_called_once()
    session.commit.assert_called_once()


def test_dashboard_collections_reconcile_all_live_queues(api_client, db_enabled):
    session = MagicMock()
    job_data = {"id": 66, "job_type": "collection_reconcile_all", "status": "queued", "terminal": False}
    connection = {"id": 1_000_001, "native_id": 1, "channel_type": "magento", "channel_code": "default"}
    with mock_db_session(session):
        with patch("main._primary_collection_job_connection", return_value=connection):
            with patch(
                "app.jobs.channel_jobs.enqueue_collection_reconcile_all_job",
                return_value=MagicMock(id=66),
            ) as enqueue:
                with patch("app.jobs.channel_jobs.job_to_dict", return_value=job_data):
                    with patch("app.jobs.channel_job_worker.run_one"):
                        response = api_client.post(
                            "/api/dashboard/collections/reconcile-all",
                            json={"dry_run": False, "category_l1": "Kitchen Cabinets"},
                        )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "queued"
    assert body["job_id"] == 66
    enqueue.assert_called_once()
    session.commit.assert_called_once()


def test_start_shopping_provision_magento_returns_partial_when_option_sync_has_errors(
    api_client, db_enabled
):
    session = MagicMock()
    with mock_db_session(session):
        with patch(
            "db.collection_landing_pages.ensure_start_shopping_master_attributes",
            return_value=0,
        ):
            with patch(
                "db.collection_landing_pages.ensure_start_shopping_magento_aliases",
                return_value=0,
            ):
                with patch(
                    "db.channel_attribute_provision.provision_channel_attributes",
                    return_value={
                        "status": "ok",
                        "channel": "magento",
                        "created": 0,
                        "linked": 0,
                        "planned": 0,
                    },
                ):
                    with patch(
                        "db.channel_remote_attributes.promote_attribute_to_options",
                        side_effect=[
                            {"status": "partial", "errors": ["Attribute shopping_collection could not be converted"]},
                            {"status": "ok", "errors": []},
                            {"status": "ok", "errors": []},
                        ],
                    ):
                        response = api_client.post(
                            "/api/dashboard/collections/start-shopping-provision-magento",
                            json={"connection_id": 1},
                        )

    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "partial"
    assert body["option_sync"]["shopping_collection"]["status"] == "partial"
    session.commit.assert_called_once()
