import asyncio
from decimal import Decimal
from unittest.mock import patch

from django.test import SimpleTestCase

from flower.utils.mingdaoyun import mappings
from flower.utils.mingdaoyun.client import MingDaoYunClient
from flower.utils.mingdaoyun.fetch import (
    fetch_customers_from_mingdaoyun,
    fetch_plate_orders_from_mingdaoyun,
    fetch_products_from_mingdaoyun,
    fetch_row_by_rowid_from_mingdaoyun,
    sync_fabric_from_mingdaoyun,
)
from flower.utils.mingdaoyun.models import Customer, Fabric, Product
from flower.utils.mingdaoyun.parsers import pick_customer, pick_fabric, pick_product


class _FakeMDYClient:
    def __init__(self, response):
        self.response = response
        self.calls = []

    async def post(self, endpoint, data=None, params=None, headers=None):
        self.calls.append(
            {
                "endpoint": endpoint,
                "data": data,
                "params": params,
                "headers": headers,
            }
        )
        return self.response


class _AsyncCM:
    def __init__(self, obj):
        self.obj = obj

    async def __aenter__(self):
        return self.obj

    async def __aexit__(self, exc_type, exc, tb):
        return False


class _FakeAiohttpResponse:
    def __init__(self, json_body):
        self._json_body = json_body
        self.raise_for_status_called = False

    def raise_for_status(self):
        self.raise_for_status_called = True

    async def json(self):
        return self._json_body


class _FakeAiohttpSession:
    def __init__(self):
        self.post_calls = []

    def post(self, url, json=None):
        resp = _FakeAiohttpResponse({"ok": True})
        self.post_calls.append({"url": url, "json": json, "response": resp})
        return _AsyncCM(resp)


class _FakeAiohttpClientSession:
    def __init__(self, *args, **kwargs):
        self.headers = kwargs.get("headers")
        self.session = _FakeAiohttpSession()

    async def __aenter__(self):
        return self.session

    async def __aexit__(self, exc_type, exc, tb):
        return False


class MingDaoYunParsersTestCase(SimpleTestCase):
    def test_pick_product_maps_fields_and_converts_numbers(self):
        row = {
            mappings.product_type_map["uid"]: "P-001",
            mappings.product_type_map["rowid"]: "row-1",
            mappings.product_type_map["created_at"]: "2025-12-01 00:00:00",
            mappings.product_type_map["name"]: "产品A",
            mappings.product_type_map["pieces"]: "12",
            mappings.product_type_map["segment_size"]: "3",
            mappings.product_type_map["unit"]: "米",
            mappings.product_type_map["color"]: "红",
            mappings.product_type_map["detail_str"]: '{"k": 1}',
        }

        product = pick_product(row)
        self.assertIsInstance(product, Product)
        self.assertEqual(product.uid, "P-001")
        self.assertEqual(product.rowid, "row-1")
        self.assertEqual(product.created_at, "2025-12-01 00:00:00")
        self.assertEqual(product.name, "产品A")
        self.assertEqual(product.pieces, 12)
        self.assertEqual(product.segment_size, Decimal("3"))
        # width 与 segment_size 当前共用同一个 controlId；保持同步代码现状
        self.assertEqual(product.width, "3")
        self.assertEqual(product.unit, "米")
        self.assertEqual(product.color, "红")
        self.assertEqual(product.detail, {"k": 1})

    def test_pick_product_invalid_numbers_become_none(self):
        row = {
            mappings.product_type_map["uid"]: "P-002",
            mappings.product_type_map["rowid"]: "row-2",
            mappings.product_type_map["created_at"]: "2025-12-01 00:00:00",
            mappings.product_type_map["name"]: "产品B",
            mappings.product_type_map["pieces"]: "invalid",
            mappings.product_type_map["segment_size"]: "",
        }

        product = pick_product(row)
        self.assertIsNone(product.pieces)
        self.assertIsNone(product.segment_size)

    def test_pick_customer_maps_fields(self):
        row = {
            mappings.customer_type_map["uid"]: "C-001",
            mappings.customer_type_map["rowid"]: "crow-1",
            mappings.customer_type_map["created_at"]: "2025-12-01 00:00:00",
            mappings.customer_type_map["name"]: "客户甲",
            mappings.customer_type_map["area"]: "杭州",
        }

        customer = pick_customer(row)
        self.assertIsInstance(customer, Customer)
        self.assertEqual(customer.uid, "C-001")
        self.assertEqual(customer.rowid, "crow-1")
        self.assertEqual(customer.created_at, "2025-12-01 00:00:00")
        self.assertEqual(customer.name, "客户甲")
        self.assertEqual(customer.area, "杭州")

    def test_pick_fabric_maps_fields(self):
        row = {
            mappings.fabric_type_map["name"]: "120克本白四面弹",
        }

        fabric = pick_fabric(row)
        self.assertIsInstance(fabric, Fabric)
        self.assertEqual(fabric.name, "120克本白四面弹")


class MingDaoYunFetchTestCase(SimpleTestCase):
    def test_fetch_products_calls_client_with_expected_payload_and_parses(self):
        response = {
            "data": {
                "rows": [
                    {
                        mappings.product_type_map["uid"]: "P-001",
                        mappings.product_type_map["rowid"]: "row-1",
                        mappings.product_type_map["created_at"]: "2025-12-01 00:00:00",
                        mappings.product_type_map["name"]: "产品A",
                        mappings.product_type_map["pieces"]: "1",
                        mappings.product_type_map["segment_size"]: "2",
                    }
                ],
                "total": 123,
            }
        }
        fake_client = _FakeMDYClient(response)

        with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
            products, total = asyncio.run(fetch_products_from_mingdaoyun(page=2, page_size=5))

        self.assertEqual(total, 123)
        self.assertEqual(len(products), 1)
        self.assertIsInstance(products[0], Product)
        self.assertEqual(products[0].uid, "P-001")

        self.assertEqual(len(fake_client.calls), 1)
        call = fake_client.calls[0]
        self.assertEqual(call["endpoint"], "/v2/open/worksheet/getFilterRows")
        self.assertEqual(call["data"]["worksheetId"], mappings.MDY_WORKSHEET_ID_PRODUCT)
        self.assertEqual(call["data"]["pageIndex"], 2)
        self.assertEqual(call["data"]["pageSize"], 5)
        self.assertEqual(call["data"]["sortId"], "ctime")
        self.assertIs(call["data"]["isAsc"], True)

    def test_fetch_customers_calls_client_with_expected_payload_and_parses(self):
        response = {
            "data": {
                "rows": [
                    {
                        mappings.customer_type_map["uid"]: "C-001",
                        mappings.customer_type_map["rowid"]: "crow-1",
                        mappings.customer_type_map["created_at"]: "2025-12-01 00:00:00",
                        mappings.customer_type_map["name"]: "客户甲",
                    }
                ],
                "total": 9,
            }
        }
        fake_client = _FakeMDYClient(response)

        with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
            customers, total = asyncio.run(fetch_customers_from_mingdaoyun(page=3, page_size=7))

        self.assertEqual(total, 9)
        self.assertEqual(len(customers), 1)
        self.assertIsInstance(customers[0], Customer)
        self.assertEqual(customers[0].uid, "C-001")

        call = fake_client.calls[0]
        self.assertEqual(call["endpoint"], "/v2/open/worksheet/getFilterRows")
        self.assertEqual(call["data"]["worksheetId"], mappings.MDY_WORKSHEET_ID_CUSTOMER)
        self.assertEqual(call["data"]["pageIndex"], 3)
        self.assertEqual(call["data"]["pageSize"], 7)

    def test_fetch_plate_orders_returns_raw_rows_and_total(self):
        rows = [{"rowid": "r1", "ctime": "2025-12-17 10:07:00"}]
        response = {"data": {"rows": rows, "total": 100}}
        fake_client = _FakeMDYClient(response)

        with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
            got_rows, total = asyncio.run(
                fetch_plate_orders_from_mingdaoyun(page=4, page_size=11, sort_id="ctime", is_asc=False)
            )

        self.assertEqual(total, 100)
        self.assertEqual(got_rows, rows)

        call = fake_client.calls[0]
        self.assertEqual(call["data"]["worksheetId"], mappings.MDY_WORKSHEET_ID_PLATE_ORDER)
        self.assertEqual(call["data"]["pageIndex"], 4)
        self.assertEqual(call["data"]["pageSize"], 11)
        self.assertEqual(call["data"]["sortId"], "ctime")
        self.assertIs(call["data"]["isAsc"], False)

    def test_fetch_products_returns_empty_when_no_data(self):
        fake_client = _FakeMDYClient({})
        with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
            products, total = asyncio.run(fetch_products_from_mingdaoyun(page=1, page_size=1))

        self.assertEqual(products, [])
        self.assertEqual(total, 0)

    def test_fetch_row_by_rowid_builds_filters_payload_and_returns_first_row(self):
        rowid = "row-xyz"
        row = {
            "rowid": rowid,
            mappings.product_type_map["uid"]: "P-ROW",
        }
        response = {"data": {"rows": [row]}}
        fake_client = _FakeMDYClient(response)

        with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
            got = asyncio.run(
                fetch_row_by_rowid_from_mingdaoyun(
                    worksheet_id=mappings.MDY_WORKSHEET_ID_PRODUCT,
                    rowid=rowid,
                )
            )

        self.assertEqual(got, row)
        self.assertEqual(len(fake_client.calls), 1)
        call = fake_client.calls[0]
        self.assertEqual(call["endpoint"], "/v2/open/worksheet/getFilterRows")
        self.assertEqual(call["data"]["worksheetId"], mappings.MDY_WORKSHEET_ID_PRODUCT)
        self.assertEqual(call["data"]["listType"], 1)
        self.assertEqual(call["data"]["sortId"], "ctime")
        self.assertIs(call["data"]["isAsc"], False)
        self.assertIs(call["data"]["notGetTotal"], True)
        self.assertEqual(
            call["data"]["filters"],
            [
                {
                    "controlId": "rowId",
                    "dataType": 2,
                    "filterType": 3,
                    "value": rowid,
                }
            ],
        )

    def test_fetch_row_by_rowid_returns_none_when_no_data(self):
        fake_client = _FakeMDYClient({})
        with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
            got = asyncio.run(
                fetch_row_by_rowid_from_mingdaoyun(
                    worksheet_id=mappings.MDY_WORKSHEET_ID_PRODUCT,
                    rowid="missing",
                )
            )
        self.assertIsNone(got)


class MingDaoYunFabricSyncTestCase(SimpleTestCase):
    def test_sync_fabric_calls_create_hook_and_returns_total(self):
        response = {
            "data": {
                "rows": [
                    {mappings.fabric_type_map["name"]: "布料A"},
                    {mappings.fabric_type_map["name"]: "布料B"},
                ],
                "total": 2,
            }
        }
        fake_client = _FakeMDYClient(response)
        called = {}

        def fake_create(page, page_size, fabrics):
            called["page"] = page
            called["page_size"] = page_size
            called["fabrics"] = fabrics

        def fake_sync_to_async(func):
            async def _wrapper(*args, **kwargs):
                return func(*args, **kwargs)

            return _wrapper

        with (
            patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client),
            patch("flower.utils.mingdaoyun.fetch._create_fabric_quick_inputs", side_effect=fake_create),
            patch("flower.utils.mingdaoyun.fetch.sync_to_async", side_effect=fake_sync_to_async),
        ):
            total = asyncio.run(sync_fabric_from_mingdaoyun(page=9, page_size=10))

        self.assertEqual(total, 2)
        self.assertEqual(called["page"], 9)
        self.assertEqual(called["page_size"], 10)
        self.assertEqual([f.name for f in called["fabrics"]], ["布料A", "布料B"])

    def test_sync_fabric_returns_zero_when_no_data(self):
        fake_client = _FakeMDYClient({})
        called = {"hit": False}

        def fake_create(*_args, **_kwargs):
            called["hit"] = True

        def fake_sync_to_async(func):
            async def _wrapper(*args, **kwargs):
                return func(*args, **kwargs)

            return _wrapper

        with (
            patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client),
            patch("flower.utils.mingdaoyun.fetch._create_fabric_quick_inputs", side_effect=fake_create),
            patch("flower.utils.mingdaoyun.fetch.sync_to_async", side_effect=fake_sync_to_async),
        ):
            total = asyncio.run(sync_fabric_from_mingdaoyun(page=1, page_size=1))

        self.assertEqual(total, 0)
        self.assertIs(called["hit"], False)


class MingDaoYunClientTestCase(SimpleTestCase):
    def test_client_post_merges_auth_params_and_passes_headers(self):
        created = {}

        def fake_client_session(*args, **kwargs):
            cs = _FakeAiohttpClientSession(*args, **kwargs)
            created["cs"] = cs
            return cs

        client = MingDaoYunClient(app_key="ak", sign="sg", base_url="https://api.mingdao.com")
        with patch("flower.utils.mingdaoyun.client.aiohttp.ClientSession", side_effect=fake_client_session):
            body = asyncio.run(
                client.post(
                    endpoint="/v2/open/worksheet/getFilterRows",
                    data={"worksheetId": "dummy", "pageIndex": 1},
                    headers={"X-Test": "1"},
                )
            )

        self.assertEqual(body, {"ok": True})

        cs = created["cs"]
        self.assertIsInstance(cs.headers, dict)
        self.assertEqual(cs.headers.get("Content-Type"), "application/json")
        self.assertEqual(cs.headers.get("X-Test"), "1")

        self.assertEqual(len(cs.session.post_calls), 1)
        call = cs.session.post_calls[0]
        self.assertEqual(call["url"], "https://api.mingdao.com/v2/open/worksheet/getFilterRows")
        self.assertEqual(call["json"]["worksheetId"], "dummy")
        self.assertEqual(call["json"]["pageIndex"], 1)
        self.assertEqual(call["json"]["appKey"], "ak")
        self.assertEqual(call["json"]["sign"], "sg")
        self.assertTrue(call["response"].raise_for_status_called)
