from decimal import Decimal
import datetime

from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.test import TestCase
from django.utils import timezone
from rest_framework.test import APIClient, APIRequestFactory

from basic_info import models as basic_models
from printing import models as printing_models
from business import models as business_models
from shipment import models as shipment_models
from api_v2.views.printing import PrintingJobByCustomerView
from django.contrib.contenttypes.models import ContentType
from stateflow import models as stateflow_models
from django.test.utils import override_settings
from mes import models as mes_models


class QuickCreateEmployeeUserAPITest(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.merchant = basic_models.Merchant.objects.create(
            name='测试商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.admin_user = get_user_model().objects.create_user(username='admin', password='pass12345')
        self.admin_employee = basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.admin_user,
            name='管理员',
        )
        self.client.force_authenticate(user=self.admin_user)
        self.url = '/api/v2/users/quick-create/'
        self.payload = {
            'username': 'new_user',
            'password': 'strongPass#1',
            'display_name': '新员工',
            'merchant_id': self.merchant.id,
            'mobile': '13800000000',
        }

    def test_quick_create_employee_user_success(self):
        response = self.client.post(self.url, self.payload, format='json')
        self.assertEqual(response.status_code, 201)
        data = response.data
        self.assertIn('user', data)
        self.assertIn('employee', data)
        self.assertEqual(data['user']['username'], self.payload['username'])

        created_user = get_user_model().objects.get(username=self.payload['username'])
        self.assertEqual(created_user.employee.merchant, self.merchant)
        self.assertEqual(created_user.employee.name, self.payload['display_name'])

    def test_quick_create_employee_user_duplicate_username(self):
        get_user_model().objects.create_user(username=self.payload['username'], password='pass12345')
        response = self.client.post(self.url, self.payload, format='json')
        self.assertEqual(response.status_code, 400)
        self.assertIn('用户名已存在', str(response.data))

    def test_quick_create_employee_user_invalid_merchant(self):
        payload = {**self.payload, 'merchant_id': 9999}
        response = self.client.post(self.url, payload, format='json')
        self.assertEqual(response.status_code, 400)
        self.assertIn('商户不存在', str(response.data))

    def test_quick_create_employee_user_with_role(self):
        """测试创建用户时指定角色"""
        role = Group.objects.create(name='管理员')
        payload = {**self.payload, 'role_id': role.id}
        response = self.client.post(self.url, payload, format='json')
        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.data['user']['role_id'], role.id)

        created_user = get_user_model().objects.get(username=self.payload['username'])
        self.assertIn(role, created_user.groups.all())

    def test_quick_create_employee_user_with_invalid_role(self):
        """测试创建用户时指定不存在的角色"""
        payload = {**self.payload, 'role_id': 99999}
        response = self.client.post(self.url, payload, format='json')
        self.assertEqual(response.status_code, 400)
        self.assertIn('角色不存在', str(response.data))


class RoleListV2APITest(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.user = get_user_model().objects.create_user(username='test_user', password='pass12345')
        self.client.force_authenticate(user=self.user)
        self.url = '/api/v2/roles/'

    def test_role_list_returns_all_roles(self):
        """测试获取所有角色"""
        role1 = Group.objects.create(name='管理员')
        role2 = Group.objects.create(name='普通用户')

        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 2)

        names = [r['name'] for r in response.data]
        self.assertIn('管理员', names)
        self.assertIn('普通用户', names)

        # 检查返回字段只有 id 和 name
        for role in response.data:
            self.assertEqual(set(role.keys()), {'id', 'name'})

    def test_role_list_empty(self):
        """测试没有角色时返回空列表"""
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data, [])

    def test_role_list_requires_authentication(self):
        """测试未认证用户无法访问"""
        client = APIClient()
        response = client.get(self.url)
        self.assertEqual(response.status_code, 401)


class HealthCheckV2APITest(TestCase):
    def setUp(self):
        self.client = APIClient()

    def test_health_check_returns_200(self):
        resp = self.client.get('/api/v2/health/')
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data.get('status'), 'ok')


class PrintingJobByCustomerAPITest(TestCase):
    def setUp(self):
        self.factory = APIRequestFactory()
        self.client = APIClient()
        self.merchant = basic_models.Merchant.objects.create(
            name='印染商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='客户X',
            created_by=None,
        )
        category = basic_models.ProductCategory.objects.create(
            merchant=self.merchant,
            name='品类',
            product_prefix='FAB',
        )
        self.product = basic_models.Product.objects.create(
            merchant=self.merchant,
            category=category,
            name='产品A',
            human_id='FAB-001',
            width_size=Decimal('150.00'),
            color='红色',
            unit=basic_models.ProductUnitEnum.METER,
        )
        self.other_product = basic_models.Product.objects.create(
            merchant=self.merchant,
            category=category,
            name='产品B',
            human_id='FAB-002',
            width_size=Decimal('160.00'),
            color='蓝色',
            unit=basic_models.ProductUnitEnum.METER,
        )
        self.printing_order = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='棉',
            width='150cm',
            curve='曲线A',
            new_curve='新曲线A',
            external_order_id='KD20410611',
        )
        self.printing_order_other = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='麻',
            width='160cm',
        )

        tz = timezone.get_default_timezone()
        in_range = timezone.make_aware(datetime.datetime(2025, 12, 5, 10, 0, 0), tz)
        out_range = timezone.make_aware(datetime.datetime(2025, 11, 20, 10, 0, 0), tz)

        self.job_in_range = printing_models.PrintingJob.objects.create(
            printing_order=self.printing_order,
            product=self.product,
            quantity=10,
            unit='米',
            sub_id=11,
        )
        printing_models.PrintingJob.objects.filter(id=self.job_in_range.id).update(created_at=in_range)

        self.job_out_range = printing_models.PrintingJob.objects.create(
            printing_order=self.printing_order_other,
            product=self.other_product,
            quantity=20,
            unit='米',
        )
        printing_models.PrintingJob.objects.filter(id=self.job_out_range.id).update(created_at=out_range)

        # 关联销售单，验证 billed_quantity
        warehouse = basic_models.WareHouse.objects.create(
            merchant=self.merchant,
            name='仓库A',
            mode=basic_models.WareHouseModeEnum.UNRESTRICTED,
        )
        operator = basic_models.Employee.objects.create(
            merchant=self.merchant,
            name='操作员',
        )
        sales_order = business_models.SalesOrder.objects.create(
            merchant=self.merchant,
            customer=self.customer,
            sales_date=datetime.date(2025, 12, 5),
            operator=operator,
            warehouse=warehouse,
        )
        business_models.SalesOrderItem.objects.create(
            sales_order=sales_order,
            product=self.product,
            price=Decimal('10'),
            quantity=Decimal('12.5'),
            unit='米',
            empty_diff_percent=Decimal('0'),
            num_of_rolls=1,
            printing_job=self.job_in_range,
        )
        business_models.SalesOrderItem.objects.create(
            sales_order=sales_order,
            product=self.product,
            price=Decimal('8'),
            quantity=Decimal('7.5'),
            unit='米',
            empty_diff_percent=Decimal('0'),
            num_of_rolls=1,
            printing_job=self.job_in_range,
        )

        self.view = PrintingJobByCustomerView.as_view()

    def _get(self, params):
        request = self.factory.get('/api/v2/printing/jobs/', params)
        return self.view(request)

    def test_basic_date_and_customer_filter(self):
        resp = self._get({
            'customer_id': self.customer.id,
            'date_from': '2025-12-01',
            'date_to': '2025-12-10',
        })
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(len(resp.data), 1)
        self.assertEqual(resp.data[0]['id'], self.job_in_range.id)
        self.assertEqual(resp.data[0]['external_order_id'], 'KD20410611')
        self.assertEqual(resp.data[0]['sub_id'], 11)
        self.assertEqual(resp.data[0]['billed_quantity'], '20.00')
        self.assertEqual(resp.data[0]['curve'], '曲线A')
        self.assertEqual(resp.data[0]['new_curve'], '新曲线A')

    def test_filter_by_printing_order(self):
        resp = self._get({
            'customer_id': self.customer.id,
            'date_from': '2025-12-01',
            'date_to': '2025-12-10',
            'printing_order': self.printing_order.id,
        })
        self.assertEqual(len(resp.data), 1)
        self.assertEqual(resp.data[0]['printing_order'], self.printing_order.id)

    def test_filter_by_product_fields(self):
        resp = self._get({
            'customer_id': self.customer.id,
            'date_from': '2025-12-01',
            'date_to': '2025-12-10',
            'product_id': self.product.id,
            'product_name': '产品A',
            'product_human_id': 'FAB-001',
            'product_width_size': '150',
            'product_color': '红',
        })
        self.assertEqual(len(resp.data), 1)
        data = resp.data[0]
        self.assertEqual(data['product']['id'], self.product.id)
        self.assertEqual(data['billed_quantity'], '20.00')


class PrintingOrderWithoutSalesOrderAPITest(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.url = '/api/v2/printing-orders/without-sales-order/'

        self.merchant = basic_models.Merchant.objects.create(
            name='印染商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.other_merchant = basic_models.Merchant.objects.create(
            name='其他商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='factory_order_user', password='pass12345')
        basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.user,
            name='工厂员工',
        )
        self.client.force_authenticate(user=self.user)

        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='客户A',
            created_by=None,
        )
        self.other_customer = basic_models.Customer.objects.create(
            merchant=self.other_merchant,
            name='客户B',
            created_by=None,
        )
        category = basic_models.ProductCategory.objects.create(
            merchant=self.merchant,
            name='品类',
            product_prefix='FAB',
        )
        self.product = basic_models.Product.objects.create(
            merchant=self.merchant,
            category=category,
            name='产品A',
            human_id='FAB-PO-001',
            unit=basic_models.ProductUnitEnum.METER,
        )
        self.warehouse = basic_models.WareHouse.objects.create(
            merchant=self.merchant,
            name='仓库A',
            mode=basic_models.WareHouseModeEnum.UNRESTRICTED,
        )
        self.operator = basic_models.Employee.objects.create(
            merchant=self.merchant,
            name='操作员',
        )

        self.unbound_order = self._create_printing_order('未绑定订单')
        self.legacy_unbound_order = self._create_printing_order('历史未绑定订单', merchant=None)
        self.active_bound_order = self._create_printing_order('已绑定有效销售单')
        self.cancelled_bound_order = self._create_printing_order('只绑定作废销售单')
        self.other_customer_order = self._create_printing_order('其他客户订单', customer=self.other_customer, merchant=self.other_merchant)

        active_job = self._create_job(self.active_bound_order)
        cancelled_job = self._create_job(self.cancelled_bound_order)
        self._create_sales_order_item(active_job, status=business_models.SalesOrderStatusEnum.PENDING)
        self._create_sales_order_item(cancelled_job, status=business_models.SalesOrderStatusEnum.CANCELLED)

    def _create_printing_order(self, fabric, customer=None, merchant='__default__'):
        if merchant == '__default__':
            merchant = self.merchant
        return printing_models.PrintingOrder.objects.create(
            merchant=merchant,
            customer=customer or self.customer,
            fabric=fabric,
            width='150cm',
        )

    def _create_job(self, printing_order):
        return printing_models.PrintingJob.objects.create(
            merchant=self.merchant,
            printing_order=printing_order,
            product=self.product,
            quantity=10,
            unit='米',
        )

    def _create_sales_order_item(self, printing_job, status):
        sales_order = business_models.SalesOrder.objects.create(
            merchant=self.merchant,
            customer=self.customer,
            sales_date=datetime.date(2026, 6, 24),
            operator=self.operator,
            warehouse=self.warehouse,
            status=status,
        )
        return business_models.SalesOrderItem.objects.create(
            sales_order=sales_order,
            product=self.product,
            printing_job=printing_job,
            price=Decimal('10'),
            quantity=Decimal('10'),
            unit='米',
            empty_diff_percent=Decimal('0'),
            num_of_rolls=1,
        )

    def test_list_orders_without_non_cancelled_sales_order(self):
        response = self.client.get(self.url, {'customer_id': self.customer.id})
        self.assertEqual(response.status_code, 200)

        result_ids = {item['id'] for item in response.data}
        self.assertIn(self.unbound_order.id, result_ids)
        self.assertIn(self.legacy_unbound_order.id, result_ids)
        self.assertIn(self.cancelled_bound_order.id, result_ids)
        self.assertNotIn(self.active_bound_order.id, result_ids)
        self.assertNotIn(self.other_customer_order.id, result_ids)

    def test_customer_id_is_required(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data['detail'], 'customer_id 为必填参数')

    def test_customer_id_must_be_integer(self):
        response = self.client.get(self.url, {'customer_id': 'abc'})
        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data['detail'], 'customer_id 必须为数字')


@override_settings(AGENT_ACCESS_KEY='agent-test-key')
class AgentUnshippedShipmentListAPITest(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.url = '/api/v2/ai/shipments/unshipped/'

        self.merchant = basic_models.Merchant.objects.create(
            name='Agent商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.other_merchant = basic_models.Merchant.objects.create(
            name='其他商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='客户A',
            area='华东',
            created_by=None,
        )
        self.other_customer = basic_models.Customer.objects.create(
            merchant=self.other_merchant,
            name='客户B',
            area='华东',
            created_by=None,
        )

        self.unshipped_target = shipment_models.Shipment.objects.create(
            merchant=self.merchant,
            customer=self.customer,
            shipment_date=datetime.date(2026, 4, 14),
            area='华东',
            address='',
            contact_name='张三',
            contact_phone='13800000000',
            remark='目标记录',
        )
        self.unshipped_other_area = shipment_models.Shipment.objects.create(
            merchant=self.merchant,
            customer=self.customer,
            shipment_date=datetime.date(2026, 4, 14),
            area='华南',
        )
        delivery = shipment_models.ShipmentDelivery.objects.create(
            merchant=self.merchant,
            driver_name='李司机',
            vehicle_trip='TRIP-001',
        )
        self.shipped_shipment = shipment_models.Shipment.objects.create(
            merchant=self.merchant,
            customer=self.customer,
            shipment_date=datetime.date(2026, 4, 14),
            area='华东',
            delivery=delivery,
        )
        self.other_merchant_shipment = shipment_models.Shipment.objects.create(
            merchant=self.other_merchant,
            customer=self.other_customer,
            shipment_date=datetime.date(2026, 4, 14),
            area='华东',
        )

    def _headers(self, key='agent-test-key'):
        return {'HTTP_AUTHORIZATION': key}

    def test_list_unshipped_shipments_success(self):
        response = self.client.get(
            self.url,
            {'merchant_id': self.merchant.id, 'area': '华东'},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 1)
        result = response.data['results'][0]
        self.assertEqual(result['id'], self.unshipped_target.id)
        self.assertEqual(result['customer_name'], '客户A')
        self.assertIsNone(result['delivery'])
        self.assertIn('sales_items', result)

    def test_list_unshipped_shipments_includes_sales_items_with_printing_job_width(self):
        category = basic_models.ProductCategory.objects.create(
            merchant=self.merchant, name='品类', product_prefix='T',
        )
        product = basic_models.Product.objects.create(
            merchant=self.merchant, category=category, name='产品甲',
        )
        printing_order = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='棉',
            width='150cm',
        )
        printing_job = printing_models.PrintingJob.objects.create(
            printing_order=printing_order,
            product=product,
            quantity=100,
            unit='米',
        )
        item_with_job = shipment_models.SalesItem.objects.create(
            merchant=self.merchant,
            shipment=self.unshipped_target,
            name='销售品甲',
            quantity=50,
            unit=shipment_models.UnitChoices.PIECE,
            printing_job_id=printing_job.id,
        )
        item_no_job = shipment_models.SalesItem.objects.create(
            merchant=self.merchant,
            shipment=self.unshipped_target,
            name='销售品乙',
            quantity=10,
            unit=shipment_models.UnitChoices.PIECE,
        )

        response = self.client.get(
            self.url,
            {'merchant_id': self.merchant.id, 'area': '华东'},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 200)
        result = response.data['results'][0]
        items_by_id = {i['id']: i for i in result['sales_items']}

        self.assertIn(item_with_job.id, items_by_id)
        self.assertEqual(items_by_id[item_with_job.id]['printing_job_width'], '150cm')
        self.assertEqual(items_by_id[item_with_job.id]['printing_job_id'], printing_job.id)

        self.assertIn(item_no_job.id, items_by_id)
        self.assertIsNone(items_by_id[item_no_job.id]['printing_job_width'])

    def test_list_unshipped_shipments_excludes_soft_deleted_sales_items(self):
        item = shipment_models.SalesItem.objects.create(
            merchant=self.merchant,
            shipment=self.unshipped_target,
            name='待删除品',
            quantity=5,
            unit=shipment_models.UnitChoices.PIECE,
            delete_at=datetime.datetime(2026, 4, 14, 0, 0, tzinfo=datetime.timezone.utc),
        )

        response = self.client.get(
            self.url,
            {'merchant_id': self.merchant.id, 'area': '华东'},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 200)
        result = response.data['results'][0]
        item_ids = [i['id'] for i in result['sales_items']]
        self.assertNotIn(item.id, item_ids)

    def test_list_unshipped_shipments_requires_access_key(self):
        response = self.client.get(
            self.url,
            {'merchant_id': self.merchant.id, 'area': '华东'},
        )
        self.assertEqual(response.status_code, 401)

    def test_list_unshipped_shipments_rejects_invalid_access_key(self):
        response = self.client.get(
            self.url,
            {'merchant_id': self.merchant.id, 'area': '华东'},
            **self._headers(key='wrong-key'),
        )
        self.assertEqual(response.status_code, 401)

    def test_list_unshipped_shipments_requires_merchant_and_area(self):
        response = self.client.get(
            self.url,
            {'area': '华东'},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 400)
        self.assertIn('merchant_id', response.data)

        response = self.client.get(
            self.url,
            {'merchant_id': self.merchant.id},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 400)
        self.assertIn('area', response.data)

    def test_list_unshipped_shipments_filters_by_merchant(self):
        response = self.client.get(
            self.url,
            {'merchant_id': self.other_merchant.id, 'area': '华东'},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 1)
        self.assertEqual(response.data['results'][0]['id'], self.other_merchant_shipment.id)

    def test_list_unshipped_shipments_supports_limit_offset(self):
        shipment_models.Shipment.objects.create(
            merchant=self.merchant,
            customer=self.customer,
            shipment_date=datetime.date(2026, 4, 15),
            area='华东',
            remark='第二条',
        )

        response = self.client.get(
            self.url,
            {'merchant_id': self.merchant.id, 'area': '华东', 'limit': 1, 'offset': 0},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 2)
        self.assertEqual(len(response.data['results']), 1)


class PrintingJobBatchAdvanceV2APITest(TestCase):
    def setUp(self):
        self.client = APIClient()

        # 工厂用户（满足 IsPrintingFactory）
        self.merchant = basic_models.Merchant.objects.create(
            name='印染工厂',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='factory_user', password='pass12345')
        self.employee = basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.user,
            name='工厂员工',
        )
        self.client.force_authenticate(user=self.user)

        # 基础客户/产品
        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='客户A',
            created_by=None,
        )
        category = basic_models.ProductCategory.objects.create(
            merchant=self.merchant,
            name='品类',
            product_prefix='FAB',
        )
        self.product = basic_models.Product.objects.create(
            merchant=self.merchant,
            category=category,
            name='产品A',
            human_id='FAB-100',
            width_size=Decimal('150.00'),
            color='红色',
            unit=basic_models.ProductUnitEnum.METER,
        )

        # stateflow：流程 + 3个节点 + 节点参数（第一步必填）
        self.state1 = stateflow_models.State.objects.create(name='待处理1', description='第一个节点')
        self.state2 = stateflow_models.State.objects.create(name='待处理2', description='第二个节点')
        self.state3 = stateflow_models.State.objects.create(name='待处理3', description='第三个节点')

        self.param_required = stateflow_models.StateParameter.objects.create(
            key='temperature',
            value='',
            is_required=True,
            description='温度（必填）',
        )
        self.state1.parameters.add(self.param_required)

        self.process = stateflow_models.Process.objects.create(name='印染流程')
        stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
        stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
        stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)

        # printing：订单 + 两条明细（每条明细都有 business_object）
        self.printing_order = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='棉',
            width='150cm',
            process=self.process,
        )
        bo1 = stateflow_models.BusinessObject.objects.create(
            name='BO-1',
            process=self.process,
        )
        bo2 = stateflow_models.BusinessObject.objects.create(
            name='BO-2',
            process=self.process,
        )

        self.job1 = printing_models.PrintingJob.objects.create(
            printing_order=self.printing_order,
            product=self.product,
            quantity=10,
            unit='米',
            business_object=bo1,
        )
        self.job2 = printing_models.PrintingJob.objects.create(
            printing_order=self.printing_order,
            product=self.product,
            quantity=20,
            unit='米',
            business_object=bo2,
        )

    def test_preview_success_returns_next_state_and_parameters(self):
        resp = self.client.post(
            '/api/v2/printing-jobs/batch-advance/preview/',
            {'printing_job_ids': [self.job1.id, self.job2.id]},
            format='json'
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['printing_order_id'], self.printing_order.id)
        self.assertEqual(set(resp.data['printing_job_ids']), {self.job1.id, self.job2.id})

        next_state = resp.data['next_state']
        self.assertEqual(next_state['id'], self.state1.id)
        self.assertEqual(next_state['order'], 0)
        self.assertTrue(any(p['key'] == 'temperature' and p['is_required'] for p in next_state['parameters']))

    def test_preview_fails_when_jobs_not_same_printing_order(self):
        other_order = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='麻',
            width='160cm',
            process=self.process,
        )
        bo3 = stateflow_models.BusinessObject.objects.create(name='BO-3', process=self.process)
        job3 = printing_models.PrintingJob.objects.create(
            printing_order=other_order,
            product=self.product,
            quantity=5,
            unit='米',
            business_object=bo3,
        )

        resp = self.client.post(
            '/api/v2/printing-jobs/batch-advance/preview/',
            {'printing_job_ids': [self.job1.id, job3.id]},
            format='json'
        )
        self.assertEqual(resp.status_code, 400)
        self.assertIn('printing_order', resp.data.get('detail', ''))

    def test_submit_success_advances_all_and_creates_batch_record(self):
        resp = self.client.post(
            '/api/v2/printing-jobs/batch-advance/',
            {
                'printing_job_ids': [self.job1.id, self.job2.id],
                'parameters': {'temperature': '25.5'}
            },
            format='json'
        )
        self.assertEqual(resp.status_code, 200)
        self.assertIn('batch_id', resp.data)

        # stateflow：两条 business_object 都应生成 1 条完成日志，且完成的是 state1
        self.job1.refresh_from_db()
        self.job2.refresh_from_db()
        # 同时应修复绑定：BusinessObject.content_type/object_id 指向 PrintingJob
        from django.contrib.contenttypes.models import ContentType
        ct_job = ContentType.objects.get_for_model(printing_models.PrintingJob)
        self.job1.business_object.refresh_from_db()
        self.job2.business_object.refresh_from_db()
        self.assertEqual(self.job1.business_object.content_type_id, ct_job.id)
        self.assertEqual(self.job1.business_object.object_id, self.job1.id)
        self.assertEqual(self.job2.business_object.content_type_id, ct_job.id)
        self.assertEqual(self.job2.business_object.object_id, self.job2.id)
        self.assertEqual(self.job1.business_object.state_logs.filter(is_cancelled=False).count(), 1)
        self.assertEqual(self.job2.business_object.state_logs.filter(is_cancelled=False).count(), 1)
        self.assertEqual(self.job1.business_object.state_logs.first().state_id, self.state1.id)
        self.assertEqual(self.job2.business_object.state_logs.first().state_id, self.state1.id)

        # printing：批量记录应存在且关联 jobs
        record = printing_models.PrintingJobBatchAdvanceRecord.objects.get(id=resp.data['batch_id'])
        self.assertEqual(record.created_by_id, self.user.id)
        self.assertEqual(record.printing_order_id, self.printing_order.id)
        self.assertEqual(record.state_id, self.state1.id)
        self.assertEqual(record.parameters.get('temperature'), '25.5')
        self.assertEqual(set(record.printing_jobs.values_list('id', flat=True)), {self.job1.id, self.job2.id})

    def test_submit_fails_when_missing_required_parameter(self):
        resp = self.client.post(
            '/api/v2/printing-jobs/batch-advance/',
            {
                'printing_job_ids': [self.job1.id, self.job2.id],
                'parameters': {}  # 缺少 temperature
            },
            format='json'
        )
        self.assertEqual(resp.status_code, 400)
        self.assertIn('缺失必填参数', resp.data.get('detail', ''))

        # 未产生任何日志/批量记录
        self.assertEqual(self.job1.business_object.state_logs.count(), 0)
        self.assertEqual(self.job2.business_object.state_logs.count(), 0)
        self.assertEqual(printing_models.PrintingJobBatchAdvanceRecord.objects.count(), 0)


class BusinessObjectCloneV2APITest(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.user = get_user_model().objects.create_user(username='u1', password='pass12345')
        self.client.force_authenticate(user=self.user)

        self.state1 = stateflow_models.State.objects.create(name='S1')
        self.state2 = stateflow_models.State.objects.create(name='S2')
        self.process = stateflow_models.Process.objects.create(name='P1')
        stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
        stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)

        self.process_src = stateflow_models.Process.objects.create(name='P-src')
        self.process_dst = stateflow_models.Process.objects.create(name='P-dst')

        ct = ContentType.objects.get_for_model(stateflow_models.Process)
        self.bo = stateflow_models.BusinessObject.objects.create(
            name='BO',
            process=self.process,
            content_type=ct,
            object_id=self.process_src.id,
        )

        # 创建一条日志 + 参数记录，确保“克隆包含工艺参数”
        log = stateflow_models.StateFlowRecord.objects.create(
            business_object=self.bo,
            state=self.state1,
            completed_by=self.user,
        )
        stateflow_models.StateLogParameterRecord.objects.create(
            state_log=log,
            parameters={'temperature': '25.5'},
            remark='r1',
        )

    def test_clone_business_object_api_returns_new_id(self):
        new_object_id = self.process_dst.id
        resp = self.client.post(
            '/api/v2/stateflow/business-objects/clone/',
            {
                'business_object_id': self.bo.id,
                'content_type': self.bo.content_type_id,
                'object_id': new_object_id,
            },
            format='json'
        )
        self.assertEqual(resp.status_code, 200)
        self.assertIn('business_object_id', resp.data)

        new_id = resp.data['business_object_id']
        self.assertNotEqual(new_id, self.bo.id)

        cloned = stateflow_models.BusinessObject.objects.get(id=new_id)
        self.assertEqual(cloned.process_id, self.bo.process_id)
        self.assertEqual(cloned.state_logs.count(), self.bo.state_logs.count())
        self.assertEqual(cloned.content_type_id, self.bo.content_type_id)
        self.assertEqual(cloned.object_id, new_object_id)

    def test_clone_business_object_api_binds_to_plate_order_business_object(self):
        """
        printing 工作流依赖：新建 PlateOrder 后通过 clone API 复制流程进度，
        后端应自动把克隆后的 BusinessObject 绑定到目标 PlateOrder.business_object。
        """
        merchant = basic_models.Merchant.objects.create(
            name='M1',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        customer = basic_models.Customer.objects.create(
            merchant=merchant,
            name='C1',
            created_by=None,
        )

        # source PlateOrder（会自动创建 business_object）
        source_po = printing_models.PlateOrder.objects.create(
            customer=customer,
            process=self.process.id,
            design_code='SRC',
            style_name='S',
        )
        self.assertIsNotNone(source_po.business_object_id)

        # 给 source 写一条日志 + 参数，确保克隆后能看到“节点与参数”
        log = stateflow_models.StateFlowRecord.objects.create(
            business_object=source_po.business_object,
            state=self.state1,
            completed_by=self.user,
        )
        stateflow_models.StateLogParameterRecord.objects.create(
            state_log=log,
            parameters={'temperature': '25.5'},
            remark='r1',
        )

        # target PlateOrder（创建时也会自动创建一个“空 business_object”）
        target_po = printing_models.PlateOrder.objects.create(
            customer=customer,
            process=self.process.id,
            design_code='DST',
            style_name='D',
        )
        self.assertIsNotNone(target_po.business_object_id)
        old_target_bo_id = target_po.business_object_id

        ct_po = ContentType.objects.get_for_model(printing_models.PlateOrder)
        resp = self.client.post(
            '/api/v2/stateflow/business-objects/clone/',
            {
                'business_object_id': source_po.business_object_id,
                'content_type': ct_po.id,
                'object_id': target_po.id,
            },
            format='json'
        )
        self.assertEqual(resp.status_code, 200, resp.data)
        new_bo_id = resp.data['business_object_id']

        target_po.refresh_from_db()
        self.assertEqual(target_po.business_object_id, new_bo_id)

        # 目标 PlateOrder 的旧“空 BO”应被安全清理（避免残留多份绑定）
        self.assertFalse(stateflow_models.BusinessObject.objects.filter(id=old_target_bo_id).exists())

        # 克隆后的日志与参数应存在
        self.assertEqual(target_po.business_object.state_logs.count(), 1)
        cloned_log = target_po.business_object.state_logs.first()
        self.assertEqual(cloned_log.state_id, self.state1.id)
        self.assertEqual(cloned_log.parameter_records.count(), 1)
        self.assertEqual(cloned_log.parameter_records.first().parameters.get('temperature'), '25.5')

    def test_clone_business_object_api_404(self):
        resp = self.client.post(
            '/api/v2/stateflow/business-objects/clone/',
            {
                'business_object_id': 999999,
                'content_type': self.bo.content_type_id,
                'object_id': 222,
            },
            format='json'
        )
        self.assertEqual(resp.status_code, 404)

    def test_clone_business_object_api_rejects_mismatched_content_type(self):
        # 用另一个 ContentType（比如 State）
        ct_state = ContentType.objects.get_for_model(stateflow_models.State)
        self.assertNotEqual(ct_state.id, self.bo.content_type_id)

        resp = self.client.post(
            '/api/v2/stateflow/business-objects/clone/',
            {
                'business_object_id': self.bo.id,
                'content_type': ct_state.id,
                'object_id': 222,
            },
            format='json'
        )
        self.assertEqual(resp.status_code, 400)
        self.assertIn('content_type', resp.data.get('detail', ''))

    def test_clone_business_object_api_unauthorized(self):
        client = APIClient()
        resp = client.post(
            '/api/v2/stateflow/business-objects/clone/',
            {
                'business_object_id': self.bo.id,
                'content_type': self.bo.content_type_id,
                'object_id': 222,
            },
            format='json'
        )
        self.assertEqual(resp.status_code, 401)


class PlateOrderByProcessNodeV2APITest(TestCase):
    def setUp(self):
        self.client = APIClient()

        # 工厂用户（满足 IsPrintingFactory）
        self.merchant = basic_models.Merchant.objects.create(
            name='开版工厂',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='plate_factory_user', password='pass12345')
        basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.user,
            name='开版员工',
        )
        self.client.force_authenticate(user=self.user)

        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='客户P',
            created_by=None,
        )

        # stateflow：流程 + 3个节点
        self.state1 = stateflow_models.State.objects.create(name='节点1')
        self.state2 = stateflow_models.State.objects.create(name='节点2')
        self.state3 = stateflow_models.State.objects.create(name='节点3')
        self.param2 = stateflow_models.StateParameter.objects.create(key='temperature', value='25')
        self.state2.parameters.add(self.param2)

        self.process = stateflow_models.Process.objects.create(name='开版流程')
        self.node1 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
        self.node2 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
        self.node3 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)

        # PlateOrder：构造不同“下一待执行节点”
        self.po_state1 = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='PO-A',
            style_name='款式A',
        )

        self.po_state2_old = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='CODE-X',  # 不包含数字，便于测试 search=id
            style_name='款式B',
        )
        stateflow_models.StateFlowRecord.objects.create(
            business_object=self.po_state2_old.business_object,
            state=self.state1,
            completed_by=self.user,
            is_cancelled=False,
        )
        # 订单维度参数：模拟该订单曾在 state2 提交过参数，但该状态记录已撤销（因此当前仍处于 state2 待执行）
        cancelled_log = stateflow_models.StateFlowRecord.objects.create(
            business_object=self.po_state2_old.business_object,
            state=self.state2,
            completed_by=self.user,
            is_cancelled=True,
        )
        stateflow_models.StateLogParameterRecord.objects.create(
            state_log=cancelled_log,
            parameters={'temperature': '30'},
        )

        self.po_state2_new = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='ABC-999',
            style_name='款式C',
        )
        stateflow_models.StateFlowRecord.objects.create(
            business_object=self.po_state2_new.business_object,
            state=self.state1,
            completed_by=self.user,
            is_cancelled=False,
        )

        self.po_state3 = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='PO-D',
            style_name='款式D',
        )
        stateflow_models.StateFlowRecord.objects.create(
            business_object=self.po_state3.business_object,
            state=self.state1,
            completed_by=self.user,
            is_cancelled=False,
        )
        stateflow_models.StateFlowRecord.objects.create(
            business_object=self.po_state3.business_object,
            state=self.state2,
            completed_by=self.user,
            is_cancelled=False,
        )

        # 固定 created_at 用于排序断言
        tz = timezone.get_default_timezone()
        t_old = timezone.make_aware(datetime.datetime(2025, 12, 1, 10, 0, 0), tz)
        t_new = timezone.make_aware(datetime.datetime(2025, 12, 2, 10, 0, 0), tz)
        printing_models.PlateOrder.objects.filter(id=self.po_state2_old.id).update(created_at=t_old)
        printing_models.PlateOrder.objects.filter(id=self.po_state2_new.id).update(created_at=t_new)

        self.url = '/api/v2/plate-orders/by-process-node/'

    def test_filter_by_process_node_returns_only_matching_next_pending(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id})
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['process_node']['id'], self.node2.id)
        self.assertEqual(resp.data['process_node']['state_id'], self.state2.id)
        self.assertTrue(any(p['key'] == 'temperature' and p['value'] == '25' for p in resp.data['parameters']))

        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(set(ids), {self.po_state3.id})

        # results 必须包含“订单维度”的参数值（同一节点下不同订单可不同）
        by_id = {item['id']: item for item in resp.data['results']}
        self.assertIn('created_by', by_id[self.po_state3.id])
        self.assertIn('process_parameters', by_id[self.po_state3.id])

        # po_state3：已完成 state2，但未提交参数，value 应为 null
        self.assertTrue(
            any(p['key'] == 'temperature' and p['value'] is None for p in by_id[self.po_state3.id]['process_parameters'])
        )

    def test_process_parameters_schema_and_keys_align_with_template(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id})
        self.assertEqual(resp.status_code, 200)

        template_keys = [p['key'] for p in resp.data['parameters']]
        self.assertGreater(len(template_keys), 0)

        for item in resp.data['results']:
            self.assertIn('process_parameters', item)
            self.assertIsInstance(item['process_parameters'], list)

            keys = [p.get('key') for p in item['process_parameters']]
            self.assertEqual(keys, template_keys)

            # 严格 schema：每项只能有 key/value 两个字段（便于前端做稳定类型定义）
            for p in item['process_parameters']:
                self.assertEqual(set(p.keys()), {'key', 'value'})

    def test_when_state_has_no_parameters_returns_empty_process_parameters(self):
        # node1 对应 state1：该测试 setUp 中未配置任何参数
        resp = self.client.get(self.url, {'process_node_id': self.node1.id})
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['parameters'], [])
        self.assertEqual(len(resp.data['results']), 3)
        ids = {item['id'] for item in resp.data['results']}
        self.assertEqual(ids, {self.po_state2_old.id, self.po_state2_new.id, self.po_state3.id})
        for item in resp.data['results']:
            self.assertEqual(item['process_parameters'], [])

    def test_default_ordering_is_minus_created_at(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id})
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids[0], self.po_state3.id)

    def test_ordering_param_created_at_asc(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'ordering': 'created_at'})
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids[0], self.po_state3.id)

    def test_search_supports_design_code_icontains(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': 'abc'})
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids, [])

    def test_search_supports_pk_without_extra_param(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': str(self.po_state2_old.id)})
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids, [])

    def test_pagination_limit_offset(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'limit': 1, 'offset': 0})
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['count'], 1)
        self.assertEqual(len(resp.data['results']), 1)

    def test_param_filter_requires_both_key_and_value(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'param_key': 'temperature'})
        self.assertEqual(resp.status_code, 400)
        self.assertIn('param_key', resp.data.get('detail', ''))

        resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'param_value': '30'})
        self.assertEqual(resp.status_code, 400)
        self.assertIn('param_value', resp.data.get('detail', ''))

    def test_param_filter_ignores_cancelled_state_logs(self):
        resp = self.client.get(
            self.url,
            {
                'process_node_id': self.node2.id,
                'param_key': 'temperature',
                'param_value': '30',
            },
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['count'], 0)
        self.assertEqual(resp.data['results'], [])

    def test_param_filter_supports_fuzzy_match(self):
        log = stateflow_models.StateFlowRecord.objects.filter(
            business_object=self.po_state3.business_object,
            state=self.state2,
            is_cancelled=False,
        ).first()
        stateflow_models.StateLogParameterRecord.objects.create(
            state_log=log,
            parameters={'temperature': '30C'},
        )

        resp = self.client.get(
            self.url,
            {
                'process_node_id': self.node2.id,
                'param_key': 'temperature',
                'param_value': '30',
            },
        )
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids, [self.po_state3.id])

    def test_date_filter_requires_both(self):
        resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'date_from': '2026-02-01'})
        self.assertEqual(resp.status_code, 400)
        self.assertIn('date_from', resp.data.get('detail', ''))

        resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'date_to': '2026-02-01'})
        self.assertEqual(resp.status_code, 400)
        self.assertIn('date_to', resp.data.get('detail', ''))

    def test_date_filter_applies(self):
        tz = timezone.get_default_timezone()
        target_dt = timezone.make_aware(datetime.datetime(2025, 12, 3, 10, 0, 0), tz)
        printing_models.PlateOrder.objects.filter(id=self.po_state3.id).update(created_at=target_dt)

        resp = self.client.get(
            self.url,
            {
                'process_node_id': self.node2.id,
                'date_from': '2025-12-03',
                'date_to': '2025-12-03',
            },
        )
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids, [self.po_state3.id])

        resp = self.client.get(
            self.url,
            {
                'process_node_id': self.node2.id,
                'date_from': '2025-12-04',
                'date_to': '2025-12-04',
            },
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['results'], [])


class PlateOrderByProcessV2APITest(TestCase):
    def setUp(self):
        self.client = APIClient()

        # 工厂用户（满足 IsPrintingFactory）
        self.merchant = basic_models.Merchant.objects.create(
            name='开版工厂2',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='plate_process_user', password='pass12345')
        basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.user,
            name='开版员工2',
        )
        self.client.force_authenticate(user=self.user)

        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='客户PP',
            created_by=None,
        )

        # stateflow：流程 + 3个节点（其中 2 个节点有参数，1 个无参数）
        self.state1 = stateflow_models.State.objects.create(name='画图')
        self.state2 = stateflow_models.State.objects.create(name='调色')
        self.state3 = stateflow_models.State.objects.create(name='套样')
        self.param_len = stateflow_models.StateParameter.objects.create(key='length', value=None)
        self.param_temp = stateflow_models.StateParameter.objects.create(key='temperature', value=None)
        self.state1.parameters.add(self.param_len)
        self.state2.parameters.add(self.param_temp)

        self.process = stateflow_models.Process.objects.create(name='开版流程2')
        self.node1 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
        self.node2 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
        self.node3 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)

        # PlateOrder：同一个 process，不同 created_at
        self.po_no_exec = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='CODE-X',  # 不包含数字，便于测试 plate_order=pk
            style_name='款式1',
        )
        self.po_exec = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='PO-EXEC',
            style_name='款式2',
        )
        # 执行 state1（非撤销）并提交参数
        log1 = stateflow_models.StateFlowRecord.objects.create(
            business_object=self.po_exec.business_object,
            state=self.state1,
            completed_by=self.user,
            is_cancelled=False,
        )
        stateflow_models.StateLogParameterRecord.objects.create(
            state_log=log1,
            parameters={'length': '5米'},
        )
        # 执行 state2（撤销）并提交参数（默认不带撤销参数 -> 应视为未执行）
        cancelled_log2 = stateflow_models.StateFlowRecord.objects.create(
            business_object=self.po_exec.business_object,
            state=self.state2,
            completed_by=self.user,
            is_cancelled=True,
        )
        stateflow_models.StateLogParameterRecord.objects.create(
            state_log=cancelled_log2,
            parameters={'temperature': '30'},
        )

        self.po_out_of_range = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='OUT',
            style_name='款式3',
        )

        # 固定 created_at
        tz = timezone.get_default_timezone()
        t1 = timezone.make_aware(datetime.datetime(2025, 12, 1, 10, 0, 0), tz)
        t2 = timezone.make_aware(datetime.datetime(2025, 12, 2, 10, 0, 0), tz)
        t0 = timezone.make_aware(datetime.datetime(2025, 11, 20, 10, 0, 0), tz)
        printing_models.PlateOrder.objects.filter(id=self.po_no_exec.id).update(created_at=t1)
        printing_models.PlateOrder.objects.filter(id=self.po_exec.id).update(created_at=t2)
        printing_models.PlateOrder.objects.filter(id=self.po_out_of_range.id).update(created_at=t0)

        self.url = '/api/v2/plate-orders/by-process/'

    def test_list_by_process_and_date_range(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'date_from': '2025-12-01',
                'date_to': '2025-12-02',
            }
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['process']['id'], self.process.id)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(set(ids), {self.po_no_exec.id, self.po_exec.id})
        # 序列化器字段：created_by 必须存在（允许为 null）
        for item in resp.data['results']:
            self.assertIn('created_by', item)

    def test_default_ordering_is_minus_created_at(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'date_from': '2025-12-01',
                'date_to': '2025-12-02',
            }
        )
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids[0], self.po_exec.id)
        self.assertEqual(ids[1], self.po_no_exec.id)

    def test_plate_order_param_supports_design_code_icontains(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'date_from': '2025-12-01',
                'date_to': '2025-12-02',
                'plate_order': 'exec',
            }
        )
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids, [self.po_exec.id])

    def test_plate_order_param_supports_pk_without_extra_param(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'date_from': '2025-12-01',
                'date_to': '2025-12-02',
                'plate_order': str(self.po_no_exec.id),
            }
        )
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids, [self.po_no_exec.id])

    def test_process_params_includes_all_nodes_and_is_executed(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'date_from': '2025-12-01',
                'date_to': '2025-12-02',
            }
        )
        self.assertEqual(resp.status_code, 200)

        by_id = {item['id']: item for item in resp.data['results']}
        po_exec = by_id[self.po_exec.id]
        po_no_exec = by_id[self.po_no_exec.id]

        # 每条 PlateOrder 都必须有 process_params，且包含所有节点（按 order）
        self.assertIn('process_params', po_exec)
        self.assertEqual([n['order'] for n in po_exec['process_params']], [0, 1, 2])
        self.assertEqual([n['node_name'] for n in po_exec['process_params']], ['画图', '调色', '套样'])

        # po_exec：state1 已执行；state2 只有撤销记录 -> 默认视为未执行；state3 未执行
        n1, n2, n3 = po_exec['process_params']
        self.assertTrue(n1['is_executed'])
        self.assertFalse(n2['is_executed'])
        self.assertFalse(n3['is_executed'])

        self.assertEqual([p['key'] for p in n1['params']], ['length'])
        self.assertEqual(n1['params'][0]['value'], '5米')
        self.assertEqual([p['key'] for p in n2['params']], ['temperature'])
        self.assertIsNone(n2['params'][0]['value'])
        self.assertEqual(n3['params'], [])

        # po_no_exec：全部未执行，参数 value 全为 null
        self.assertEqual([n['order'] for n in po_no_exec['process_params']], [0, 1, 2])
        self.assertFalse(po_no_exec['process_params'][0]['is_executed'])
        self.assertIsNone(po_no_exec['process_params'][0]['params'][0]['value'])

    def test_invalid_ordering_returns_400(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'date_from': '2025-12-01',
                'date_to': '2025-12-02',
                'ordering': 'unknown',
            }
        )
        self.assertEqual(resp.status_code, 400)


class PlateOrderByStateStatusV2APITest(TestCase):
    def setUp(self):
        self.client = APIClient()

        self.merchant = basic_models.Merchant.objects.create(
            name='开版工厂3',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='state_status_user', password='pass12345')
        basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.user,
            name='开版员工3',
        )
        self.client.force_authenticate(user=self.user)

        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='客户SS',
            created_by=None,
        )

        self.state_prepare = stateflow_models.State.objects.create(name='画图')
        self.state_target = stateflow_models.State.objects.create(name='调色')
        self.state_unlinked = stateflow_models.State.objects.create(name='未绑定')
        self.param_temp = stateflow_models.StateParameter.objects.create(key='temperature', value='25')
        self.state_target.parameters.add(self.param_temp)

        self.process = stateflow_models.Process.objects.create(name='主流程')
        self.node_prepare = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state_prepare, order=0)
        self.node_target = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state_target, order=1)

        self.other_process = stateflow_models.Process.objects.create(name='其他流程')
        stateflow_models.ProcessNode.objects.create(process=self.other_process, state=self.state_target, order=0)

        self.po_not_started = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='PO-NOT',
            style_name='款式N',
        )
        self.po_no_logs = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='PO-ZERO',
            style_name='款式Z',
        )
        self.po_completed_old = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='PO-C-OLD',
            style_name='款式CO',
        )
        self.po_completed_new = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='PO-C-NEW',
            style_name='款式CN',
        )
        self.po_cancelled = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.process.id,
            design_code='PO-CAN',
            style_name='款式CA',
        )
        self.po_other_process = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            process=self.other_process.id,
            design_code='PO-OTHER',
            style_name='款式OP',
        )

        self._complete_state(self.po_not_started, self.state_prepare)
        self._complete_state(self.po_completed_old, self.state_prepare)
        self._complete_state(self.po_completed_new, self.state_prepare)
        self._complete_state(self.po_cancelled, self.state_prepare)

        self._complete_state(self.po_completed_old, self.state_target, parameters={'temperature': '28'})
        self._complete_state(self.po_completed_new, self.state_target, parameters={'temperature': '32'})
        self._complete_state(self.po_cancelled, self.state_target, is_cancelled=True, parameters={'temperature': '99'})
        self._complete_state(self.po_other_process, self.state_target, parameters={'temperature': '88'})

        tz = timezone.get_default_timezone()
        t_old = timezone.make_aware(datetime.datetime(2025, 12, 1, 9, 0, 0), tz)
        t_new = timezone.make_aware(datetime.datetime(2025, 12, 3, 9, 0, 0), tz)
        printing_models.PlateOrder.objects.filter(id=self.po_completed_old.id).update(created_at=t_old)
        printing_models.PlateOrder.objects.filter(id=self.po_completed_new.id).update(created_at=t_new)

        self.url = '/api/v2/plate-orders/by-state-status/'

    def _complete_state(self, plate_order, state, *, is_cancelled=False, parameters=None):
        log = stateflow_models.StateFlowRecord.objects.create(
            business_object=plate_order.business_object,
            state=state,
            completed_by=self.user,
            is_cancelled=is_cancelled,
        )
        if parameters is not None:
            stateflow_models.StateLogParameterRecord.objects.create(
                state_log=log,
                parameters=parameters,
            )
        return log

    def test_completed_status_returns_sorted_plate_orders(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'state_id': self.state_target.id,
                'status': 'completed',
            }
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['state']['id'], self.state_target.id)
        self.assertEqual(resp.data['status'], 'completed')
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids, [self.po_completed_new.id, self.po_completed_old.id])

        template_keys = [p['key'] for p in resp.data['state_parameters']]
        self.assertEqual(template_keys, ['temperature'])

        by_id = {item['id']: item for item in resp.data['results']}
        self.assertEqual(by_id[self.po_completed_new.id]['state_parameters'][0]['value'], '32')
        self.assertEqual(by_id[self.po_completed_old.id]['state_parameters'][0]['value'], '28')
        self.assertIsNotNone(by_id[self.po_completed_new.id]['state_log'])
        self.assertFalse(by_id[self.po_completed_new.id]['state_log']['is_cancelled'])

    def test_default_status_is_completed(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'state_id': self.state_target.id,
            }
        )
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(set(ids), {self.po_completed_new.id, self.po_completed_old.id})

    def test_state_id_with_not_started_status_is_invalid(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'state_id': self.state_target.id,
                'status': 'not_started',
            }
        )
        self.assertEqual(resp.status_code, 400)

    def test_cancelled_status_returns_orders_with_cancelled_logs(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'state_id': self.state_target.id,
                'status': 'cancelled',
            }
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['results'][0]['id'], self.po_cancelled.id)
        self.assertTrue(resp.data['results'][0]['state_log']['is_cancelled'])

    def test_invalid_process_state_pair_returns_400(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'state_id': self.state_unlinked.id,
                'status': 'completed',
            }
        )
        self.assertEqual(resp.status_code, 400)
        self.assertIn('state 不属于该 process', resp.data['detail'])

    def test_ordering_created_at_and_pagination(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'state_id': self.state_target.id,
                'status': 'completed',
                'ordering': 'created_at',
                'limit': 1,
                'offset': 1,
            }
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['count'], 2)
        self.assertEqual(len(resp.data['results']), 1)
        self.assertEqual(resp.data['results'][0]['id'], self.po_completed_new.id)

    def test_other_process_records_are_excluded(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'state_id': self.state_target.id,
                'status': 'completed',
            }
        )
        ids = [item['id'] for item in resp.data['results']]
        self.assertNotIn(self.po_other_process.id, ids)

    def test_completed_status_only_returns_latest_state(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'state_id': self.state_prepare.id,
                'status': 'completed',
            }
        )
        self.assertEqual(resp.status_code, 200)
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(set(ids), {self.po_not_started.id, self.po_cancelled.id})
        self.assertNotIn(self.po_completed_old.id, ids)
        self.assertNotIn(self.po_completed_new.id, ids)

    def test_without_state_id_returns_orders_without_any_logs(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
            }
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['status'], 'not_started')
        self.assertIsNone(resp.data['state'])
        self.assertEqual(resp.data['state_parameters'], [])
        ids = [item['id'] for item in resp.data['results']]
        self.assertEqual(ids, [self.po_no_logs.id])
        self.assertEqual(resp.data['results'][0]['state_parameters'], [])
        self.assertIsNone(resp.data['results'][0]['state_log'])

    def test_without_state_id_only_allows_not_started_status(self):
        resp = self.client.get(
            self.url,
            {
                'process_id': self.process.id,
                'status': 'completed',
            }
        )
        self.assertEqual(resp.status_code, 400)
        self.assertIn('state_id 为空时仅支持 status=not_started', resp.data['detail'])


class PlateOrderBatchUpdateV2APITest(TestCase):
    def setUp(self):
        self.client = APIClient()

        # 工厂用户（满足 IsPrintingFactory）
        self.merchant = basic_models.Merchant.objects.create(
            name='印染工厂',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='factory_user2', password='pass12345')
        basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.user,
            name='工厂员工2',
        )
        self.client.force_authenticate(user=self.user)

        # 赋予 change 权限（批量更新必需）
        perm_change = Permission.objects.get(codename='change_plateorder')
        self.user.user_permissions.add(perm_change)

        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='客户A',
            created_by=None,
        )
        self.designer = basic_models.Employee.objects.create(
            merchant=self.merchant,
            name='设计师',
        )

        self.po1 = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            design_code='D1',
            urgency_level='正常',
            style_name='S1',
        )
        self.po2 = printing_models.PlateOrder.objects.create(
            customer=self.customer,
            design_code='D2',
            urgency_level='正常',
            style_name='S2',
        )

    def test_batch_update_success(self):
        resp = self.client.post(
            '/api/v2/plate-orders/batch-update/',
            {
                'plate_order_ids': [self.po1.id, self.po2.id],
                'data': {'urgency_level': '加急', 'designer': self.designer.id},
            },
            format='json',
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.data['updated_count'], 2)

        self.po1.refresh_from_db()
        self.po2.refresh_from_db()
        self.assertEqual(self.po1.urgency_level, '加急')
        self.assertEqual(self.po2.urgency_level, '加急')
        self.assertEqual(self.po1.designer_id, self.designer.id)
        self.assertEqual(self.po2.designer_id, self.designer.id)

    def test_batch_update_fails_when_missing_ids(self):
        resp = self.client.post(
            '/api/v2/plate-orders/batch-update/',
            {
                'plate_order_ids': [self.po1.id, 999999],
                'data': {'urgency_level': '加急'},
            },
            format='json',
        )
        self.assertEqual(resp.status_code, 400)
        self.assertIn('不存在', resp.data.get('detail', ''))

    def test_batch_update_fails_when_unknown_field(self):
        resp = self.client.post(
            '/api/v2/plate-orders/batch-update/',
            {
                'plate_order_ids': [self.po1.id],
                'data': {'process': 123},
            },
            format='json',
        )
        self.assertEqual(resp.status_code, 400)
        self.assertIn('不支持批量更新字段', resp.data.get('detail', ''))

    def test_batch_update_is_invalid_requires_permission(self):
        # 未授予 can_invalidate_plateorder
        resp = self.client.post(
            '/api/v2/plate-orders/batch-update/',
            {
                'plate_order_ids': [self.po1.id],
                'data': {'is_invalid': True},
            },
            format='json',
        )
        self.assertEqual(resp.status_code, 403)

        perm_invalidate = Permission.objects.get(codename='can_invalidate_plateorder')
        self.user.user_permissions.add(perm_invalidate)

        resp2 = self.client.post(
            '/api/v2/plate-orders/batch-update/',
            {
                'plate_order_ids': [self.po1.id],
                'data': {'is_invalid': True},
            },
            format='json',
        )
        self.assertEqual(resp2.status_code, 200)
        self.po1.refresh_from_db()
        self.assertTrue(self.po1.is_invalid)


class MyVisiblePagesAPITest(TestCase):
    """测试获取当前用户可见页面 API"""

    def setUp(self):
        self.client = APIClient()
        self.merchant = basic_models.Merchant.objects.create(
            name='测试商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='testuser', password='pass12345')
        
        # 创建前端页面
        self.main_menu = basic_models.FrontendPage.objects.create(
            key='production',
            label='生产管理',
            page_type=basic_models.FrontendPageTypeEnum.MAIN,
            sort_order=100
        )
        self.sub_menu1 = basic_models.FrontendPage.objects.create(
            key='production-kanban',
            label='生产看板',
            page_type=basic_models.FrontendPageTypeEnum.SUB,
            parent=self.main_menu,
            sort_order=101
        )
        self.sub_menu2 = basic_models.FrontendPage.objects.create(
            key='printing-order-list',
            label='生产订单',
            page_type=basic_models.FrontendPageTypeEnum.SUB,
            parent=self.main_menu,
            sort_order=102
        )
        self.other_menu = basic_models.FrontendPage.objects.create(
            key='inventory',
            label='库存管理',
            page_type=basic_models.FrontendPageTypeEnum.MAIN,
            sort_order=200
        )
        
        # 创建岗位并分配页面
        self.emp_type = basic_models.EmployeeType.objects.create(
            merchant=self.merchant,
            title='生产主管'
        )
        self.emp_type.visible_pages.add(self.main_menu)
        
        # 创建员工并关联用户
        self.employee = basic_models.Employee.objects.create(
            merchant=self.merchant,
            name='张三',
            position=self.emp_type,
            sys_user=self.user  # 关联到用户
        )
        
        self.client.force_authenticate(user=self.user)

    def test_get_visible_pages_success(self):
        """测试成功获取当前用户可见页面"""
        response = self.client.get('/api/v2/me/visible-pages/')
        self.assertEqual(response.status_code, 200)
        
        data = response.data
        self.assertEqual(data['employee_id'], self.employee.id)
        self.assertEqual(data['employee_name'], '张三')
        self.assertEqual(data['position_id'], self.emp_type.id)
        self.assertEqual(data['position_title'], '生产主管')
        
        # 验证可见页面（主菜单 + 子菜单）
        visible_keys = data['visible_keys']
        self.assertIn('production', visible_keys)
        self.assertIn('production-kanban', visible_keys)
        self.assertIn('printing-order-list', visible_keys)
        self.assertNotIn('inventory', visible_keys)
        
        # 验证页面详情
        self.assertEqual(len(data['visible_pages']), 3)

    def test_get_visible_pages_with_sub_menu_only(self):
        """测试仅授予子菜单时只返回该子菜单"""
        # 重新配置岗位：只授予子菜单
        self.emp_type.visible_pages.clear()
        self.emp_type.visible_pages.add(self.sub_menu1)
        
        response = self.client.get('/api/v2/me/visible-pages/')
        self.assertEqual(response.status_code, 200)
        
        visible_keys = response.data['visible_keys']
        self.assertIn('production-kanban', visible_keys)
        self.assertNotIn('production', visible_keys)
        self.assertNotIn('printing-order-list', visible_keys)
        self.assertEqual(len(visible_keys), 1)

    def test_get_visible_pages_employee_no_position(self):
        """测试没有岗位的员工返回空列表"""
        # 移除员工岗位
        self.employee.position = None
        self.employee.save()
        
        response = self.client.get('/api/v2/me/visible-pages/')
        self.assertEqual(response.status_code, 200)
        
        data = response.data
        self.assertEqual(data['employee_id'], self.employee.id)
        self.assertEqual(data['position_id'], None)
        self.assertEqual(data['visible_pages'], [])
        self.assertEqual(data['visible_keys'], [])

    def test_get_visible_pages_user_no_employee(self):
        """测试用户未关联员工时返回空列表"""
        # 创建一个没有关联员工的用户
        user_no_employee = get_user_model().objects.create_user(username='noemployee', password='pass12345')
        self.client.force_authenticate(user=user_no_employee)
        
        response = self.client.get('/api/v2/me/visible-pages/')
        self.assertEqual(response.status_code, 200)
        
        data = response.data
        self.assertEqual(data['employee_id'], None)
        self.assertEqual(data['employee_name'], None)
        self.assertEqual(data['position_id'], None)
        self.assertEqual(data['visible_pages'], [])
        self.assertEqual(data['visible_keys'], [])

    def test_get_visible_pages_unauthenticated(self):
        """测试未登录时返回 401"""
        self.client.logout()
        response = self.client.get('/api/v2/me/visible-pages/')
        self.assertEqual(response.status_code, 401)


class PrintingOrderBatchAdvanceRecordsAPITest(TestCase):
    """测试印染订单批量推进记录查询 API"""

    def setUp(self):
        self.client = APIClient()
        self.merchant = basic_models.Merchant.objects.create(
            name='测试商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='testuser', password='pass12345')
        self.client.force_authenticate(user=self.user)
        
        # 创建客户
        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='测试客户'
        )
        
        # 创建产品类别和产品
        self.category = basic_models.ProductCategory.objects.create(
            merchant=self.merchant,
            name='测试类别'
        )
        self.product = basic_models.Product.objects.create(
            merchant=self.merchant,
            name='测试产品',
            category=self.category,
            unit=basic_models.ProductUnitEnum.METER
        )
        
        # 创建印染订单
        self.printing_order = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='测试面料',
            width='150cm'
        )
        
        # 创建印染明细
        self.job1 = printing_models.PrintingJob.objects.create(
            printing_order=self.printing_order,
            product=self.product,
            quantity=100,
            unit='米'
        )
        self.job2 = printing_models.PrintingJob.objects.create(
            printing_order=self.printing_order,
            product=self.product,
            quantity=200,
            unit='米'
        )
        
        # 创建流程状态
        self.state = stateflow_models.State.objects.create(
            name='染色',
            description='染色工序'
        )
        
        # 创建批量推进记录
        self.record = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
            printing_order=self.printing_order,
            state=self.state,
            created_by=self.user,
            parameters={'temperature': '25.5', 'operator': '张三'}
        )
        self.record.printing_jobs.add(self.job1, self.job2)

    def test_get_batch_advance_records_success(self):
        """测试成功获取批量推进记录"""
        response = self.client.get(f'/api/v2/printing-orders/{self.printing_order.id}/batch-advance-records/')
        self.assertEqual(response.status_code, 200)
        
        data = response.data
        self.assertEqual(data['printing_order_id'], self.printing_order.id)
        self.assertEqual(data['total_count'], 1)
        self.assertEqual(len(data['records']), 1)
        
        record = data['records'][0]
        self.assertEqual(record['id'], self.record.id)
        self.assertEqual(record['state_id'], self.state.id)
        self.assertEqual(record['state_name'], '染色')
        self.assertEqual(record['created_by'], self.user.id)
        self.assertEqual(record['created_by_username'], 'testuser')
        self.assertEqual(record['parameters'], {'temperature': '25.5', 'operator': '张三'})
        self.assertEqual(record['printing_job_count'], 2)
        self.assertIn(self.job1.id, record['printing_job_ids'])
        self.assertIn(self.job2.id, record['printing_job_ids'])

    def test_get_batch_advance_records_empty(self):
        """测试没有批量推进记录时返回空列表"""
        # 创建另一个没有记录的订单
        another_order = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='测试面料2',
            width='160cm'
        )
        
        response = self.client.get(f'/api/v2/printing-orders/{another_order.id}/batch-advance-records/')
        self.assertEqual(response.status_code, 200)
        
        data = response.data
        self.assertEqual(data['printing_order_id'], another_order.id)
        self.assertEqual(data['total_count'], 0)
        self.assertEqual(data['records'], [])

    def test_get_batch_advance_records_order_not_found(self):
        """测试订单不存在时返回 404"""
        response = self.client.get('/api/v2/printing-orders/99999/batch-advance-records/')
        self.assertEqual(response.status_code, 404)
        self.assertIn('error', response.data)

    def test_get_batch_advance_records_multiple_records(self):
        """测试多条批量推进记录按时间倒序返回"""
        # 创建第二条记录
        state2 = stateflow_models.State.objects.create(name='整理', description='整理工序')
        record2 = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
            printing_order=self.printing_order,
            state=state2,
            created_by=self.user,
            parameters={'notes': '完成'}
        )
        record2.printing_jobs.add(self.job1)
        
        response = self.client.get(f'/api/v2/printing-orders/{self.printing_order.id}/batch-advance-records/')
        self.assertEqual(response.status_code, 200)
        
        data = response.data
        self.assertEqual(data['total_count'], 2)
        # 最新的记录应该在前面
        self.assertEqual(data['records'][0]['id'], record2.id)
        self.assertEqual(data['records'][1]['id'], self.record.id)

    def test_get_batch_advance_records_unauthenticated(self):
        """测试未登录时返回 401"""
        self.client.logout()
        response = self.client.get(f'/api/v2/printing-orders/{self.printing_order.id}/batch-advance-records/')
        self.assertEqual(response.status_code, 401)


class PrintingJobBatchAddParametersAPITest(TestCase):
    """测试批量补充工艺参数 API"""

    def setUp(self):
        self.client = APIClient()
        self.merchant = basic_models.Merchant.objects.create(
            name='测试工厂',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='factory_user', password='pass12345')
        self.employee = basic_models.Employee.objects.create(
            merchant=self.merchant,
            name='工厂员工',
            sys_user=self.user
        )
        self.client.force_authenticate(user=self.user)
        
        # 创建客户
        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='测试客户'
        )
        
        # 创建产品类别和产品
        self.category = basic_models.ProductCategory.objects.create(
            merchant=self.merchant,
            name='测试类别'
        )
        self.product = basic_models.Product.objects.create(
            merchant=self.merchant,
            name='测试产品',
            category=self.category,
            unit=basic_models.ProductUnitEnum.METER
        )
        
        # 创建流程状态
        self.state1 = stateflow_models.State.objects.create(name='染色', description='染色工序')
        self.state2 = stateflow_models.State.objects.create(name='整理', description='整理工序')
        
        # 创建流程
        self.process = stateflow_models.Process.objects.create(name='印染流程')
        stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=1)
        stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=2)
        
        # 创建印染订单
        self.printing_order = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='测试面料',
            width='150cm',
            process=self.process
        )
        
        # 创建印染明细并绑定流程实例
        self.job1 = printing_models.PrintingJob.objects.create(
            printing_order=self.printing_order,
            product=self.product,
            quantity=100,
            unit='米'
        )
        self.job2 = printing_models.PrintingJob.objects.create(
            printing_order=self.printing_order,
            product=self.product,
            quantity=200,
            unit='米'
        )
        
        # 创建业务对象并推进到 state1
        from stateflow import services as stateflow_services
        from django.contrib.contenttypes.models import ContentType
        
        ct = ContentType.objects.get_for_model(printing_models.PrintingJob)
        
        self.bo1 = stateflow_models.BusinessObject.objects.create(
            name=f'PrintingJob-{self.job1.id}',
            process=self.process,
            content_type=ct,
            object_id=self.job1.id
        )
        self.job1.business_object = self.bo1
        self.job1.save()
        
        self.bo2 = stateflow_models.BusinessObject.objects.create(
            name=f'PrintingJob-{self.job2.id}',
            process=self.process,
            content_type=ct,
            object_id=self.job2.id
        )
        self.job2.business_object = self.bo2
        self.job2.save()
        
        # 推进到 state1（创建 state_log）
        stateflow_services.advance_to_next_state(self.bo1, self.user, temperature='25.0')
        stateflow_services.advance_to_next_state(self.bo2, self.user, temperature='25.0')

    def test_batch_add_parameters_success(self):
        """测试成功批量补充参数"""
        response = self.client.post(
            '/api/v2/printing-jobs/batch-add-parameters/',
            {
                'printing_job_ids': [self.job1.id, self.job2.id],
                'state_id': self.state1.id,
                'parameters': {'temperature': '26.0', 'operator': '李四'},
                'remark': '补测数据'
            },
            format='json'
        )
        
        self.assertEqual(response.status_code, 200)
        data = response.data
        self.assertEqual(data['detail'], '批量补充参数成功')
        self.assertEqual(data['printing_order_id'], self.printing_order.id)
        self.assertEqual(set(data['printing_job_ids']), {self.job1.id, self.job2.id})
        self.assertEqual(data['state_id'], self.state1.id)
        self.assertEqual(data['state_name'], '染色')
        self.assertEqual(data['affected_count'], 2)
        
        # 验证创建了批量操作记录
        record = printing_models.PrintingJobBatchAdvanceRecord.objects.get(id=data['batch_record_id'])
        self.assertTrue(record.only_parameters)
        self.assertEqual(record.parameters, {'temperature': '26.0', 'operator': '李四'})
        self.assertEqual(record.printing_jobs.count(), 2)

    def test_batch_add_parameters_empty_parameters(self):
        """测试参数为空时返回错误"""
        response = self.client.post(
            '/api/v2/printing-jobs/batch-add-parameters/',
            {
                'printing_job_ids': [self.job1.id],
                'state_id': self.state1.id,
                'parameters': {},
            },
            format='json'
        )
        
        self.assertEqual(response.status_code, 400)

    def test_batch_add_parameters_job_not_found(self):
        """测试 job 不存在时返回错误"""
        response = self.client.post(
            '/api/v2/printing-jobs/batch-add-parameters/',
            {
                'printing_job_ids': [99999],
                'state_id': self.state1.id,
                'parameters': {'temperature': '26.0'},
            },
            format='json'
        )
        
        self.assertEqual(response.status_code, 400)
        self.assertIn('missing_jobs', response.data)

    def test_batch_add_parameters_state_not_found(self):
        """测试 state 不存在时返回错误"""
        response = self.client.post(
            '/api/v2/printing-jobs/batch-add-parameters/',
            {
                'printing_job_ids': [self.job1.id],
                'state_id': 99999,
                'parameters': {'temperature': '26.0'},
            },
            format='json'
        )
        
        self.assertEqual(response.status_code, 400)

    def test_batch_add_parameters_missing_state_log(self):
        """测试 job 没有对应 state_log 时返回错误"""
        # state2 的 state_log 尚未创建（还没推进到那一步）
        response = self.client.post(
            '/api/v2/printing-jobs/batch-add-parameters/',
            {
                'printing_job_ids': [self.job1.id],
                'state_id': self.state2.id,  # 还没推进到这个状态
                'parameters': {'temperature': '26.0'},
            },
            format='json'
        )
        
        self.assertEqual(response.status_code, 400)
        self.assertIn('missing_state_log', response.data)

    def test_batch_add_parameters_different_orders(self):
        """测试 jobs 属于不同订单时返回错误"""
        # 创建另一个订单和 job
        another_order = printing_models.PrintingOrder.objects.create(
            customer=self.customer,
            fabric='另一面料',
            width='160cm'
        )
        another_job = printing_models.PrintingJob.objects.create(
            printing_order=another_order,
            product=self.product,
            quantity=300,
            unit='米'
        )
        
        response = self.client.post(
            '/api/v2/printing-jobs/batch-add-parameters/',
            {
                'printing_job_ids': [self.job1.id, another_job.id],
                'state_id': self.state1.id,
                'parameters': {'temperature': '26.0'},
            },
            format='json'
        )
        
        self.assertEqual(response.status_code, 400)

    def test_batch_add_parameters_unauthenticated(self):
        """测试未登录时返回 401"""
        self.client.logout()
        response = self.client.post(
            '/api/v2/printing-jobs/batch-add-parameters/',
            {
                'printing_job_ids': [self.job1.id],
                'state_id': self.state1.id,
                'parameters': {'temperature': '26.0'},
            },
            format='json'
        )
        self.assertEqual(response.status_code, 401)


@override_settings(AGENT_ACCESS_KEY='agent-test-key')
class AgentTransportVehicleAPITest(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.list_url = '/api/v2/ai/transport-vehicles/'

        self.merchant = basic_models.Merchant.objects.create(
            name='车辆商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.other_merchant = basic_models.Merchant.objects.create(
            name='其他商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )

        self.vehicle_a = basic_models.TransportVehicle.objects.create(
            merchant=self.merchant,
            name='大卡车',
            license_plate='粤A12345',
        )
        self.cap_a1 = basic_models.TransportVehicleMaterialCapacity.objects.create(
            merchant=self.merchant,
            transport_vehicle=self.vehicle_a,
            material_name='坯布',
            capacity=500,
        )
        self.cap_a2 = basic_models.TransportVehicleMaterialCapacity.objects.create(
            merchant=self.merchant,
            transport_vehicle=self.vehicle_a,
            material_name='成品',
            capacity=300,
        )
        self.vehicle_b = basic_models.TransportVehicle.objects.create(
            merchant=self.merchant,
            name='小货车',
            license_plate='粤B67890',
        )
        self.other_vehicle = basic_models.TransportVehicle.objects.create(
            merchant=self.other_merchant,
            name='他商户车',
            license_plate='粤A12345',  # same plate, different merchant
        )

    def _headers(self, key='agent-test-key'):
        return {'HTTP_AUTHORIZATION': key}

    # --- list ---

    def test_list_vehicles_success(self):
        response = self.client.get(
            self.list_url,
            {'merchant_id': self.merchant.id},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 2)
        ids = [r['id'] for r in response.data['results']]
        self.assertIn(self.vehicle_a.id, ids)
        self.assertIn(self.vehicle_b.id, ids)

    def test_list_vehicles_includes_material_capacities(self):
        response = self.client.get(
            self.list_url,
            {'merchant_id': self.merchant.id},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 200)
        vehicle_a_data = next(r for r in response.data['results'] if r['id'] == self.vehicle_a.id)
        material_names = {c['material_name'] for c in vehicle_a_data['material_capacities']}
        self.assertEqual(material_names, {'坯布', '成品'})

    def test_list_vehicles_isolates_by_merchant(self):
        response = self.client.get(
            self.list_url,
            {'merchant_id': self.other_merchant.id},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 1)
        self.assertEqual(response.data['results'][0]['id'], self.other_vehicle.id)

    def test_list_vehicles_requires_merchant_id(self):
        response = self.client.get(self.list_url, **self._headers())
        self.assertEqual(response.status_code, 400)
        self.assertIn('merchant_id', response.data)

    def test_list_vehicles_rejects_nonexistent_merchant(self):
        response = self.client.get(
            self.list_url,
            {'merchant_id': 999999},
            **self._headers(),
        )
        self.assertEqual(response.status_code, 400)

    def test_list_vehicles_requires_access_key(self):
        response = self.client.get(self.list_url, {'merchant_id': self.merchant.id})
        self.assertEqual(response.status_code, 401)

    # --- detail ---

    def test_detail_vehicle_success(self):
        url = f'/api/v2/ai/transport-vehicles/{self.vehicle_a.license_plate}/'
        response = self.client.get(url, {'merchant_id': self.merchant.id}, **self._headers())
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['id'], self.vehicle_a.id)
        self.assertEqual(response.data['license_plate'], '粤A12345')
        capacities = {c['material_name']: c['capacity'] for c in response.data['material_capacities']}
        self.assertEqual(capacities, {'坯布': 500, '成品': 300})

    def test_detail_vehicle_same_plate_different_merchant_returns_404(self):
        # other_merchant also has '粤A12345', but querying with self.merchant should return vehicle_a
        url = f'/api/v2/ai/transport-vehicles/{self.vehicle_a.license_plate}/'
        response = self.client.get(url, {'merchant_id': self.other_merchant.id}, **self._headers())
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['id'], self.other_vehicle.id)

    def test_detail_vehicle_not_found_returns_404(self):
        url = '/api/v2/ai/transport-vehicles/粤Z99999/'
        response = self.client.get(url, {'merchant_id': self.merchant.id}, **self._headers())
        self.assertEqual(response.status_code, 404)

    def test_detail_vehicle_requires_access_key(self):
        url = f'/api/v2/ai/transport-vehicles/{self.vehicle_a.license_plate}/'
        response = self.client.get(url, {'merchant_id': self.merchant.id})
        self.assertEqual(response.status_code, 401)


@override_settings(AGENT_ACCESS_KEY='agent-test-key')
class AgentMesAPITest(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.device_list_url = '/api/v2/ai/mes/devices/'
        self.assignment_list_url = '/api/v2/ai/mes/production-assignments/'

        self.merchant = basic_models.Merchant.objects.create(
            name='MES Agent 商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.other_merchant = basic_models.Merchant.objects.create(
            name='其他 MES 商户',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.user = get_user_model().objects.create_user(username='agent-mes-user', password='pass12345')
        self.other_user = get_user_model().objects.create_user(username='agent-mes-other-user', password='pass12345')
        self.employee = basic_models.Employee.objects.create(merchant=self.merchant, sys_user=self.user, name='MES员工')
        self.other_employee = basic_models.Employee.objects.create(merchant=self.other_merchant, sys_user=self.other_user, name='其他MES员工')

        self.category = mes_models.DeviceCategory.objects.create(
            merchant=self.merchant,
            name='打印机',
            created_by=self.user,
            operator=self.employee,
        )
        self.device_a = mes_models.Device.objects.create(
            merchant=self.merchant,
            category=self.category,
            name='设备A',
            peak_capacity=120,
            capacity_unit=mes_models.CapacityUnitEnum.METER,
            extra={'capacity': {'per_hour': 120}},
            created_by=self.user,
            operator=self.employee,
        )
        self.device_b = mes_models.Device.objects.create(
            merchant=self.merchant,
            category=self.category,
            name='设备B',
            peak_capacity=90,
            capacity_unit=mes_models.CapacityUnitEnum.YARD,
            created_by=self.user,
            operator=self.employee,
        )
        self.other_category = mes_models.DeviceCategory.objects.create(
            merchant=self.other_merchant,
            name='他商户分类',
            created_by=self.other_user,
            operator=self.other_employee,
        )
        self.other_device = mes_models.Device.objects.create(
            merchant=self.other_merchant,
            category=self.other_category,
            name='他商户设备',
            peak_capacity=60,
            created_by=self.other_user,
            operator=self.other_employee,
        )

        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='测试客户',
            mobile='13800138011',
        )
        self.other_customer = basic_models.Customer.objects.create(
            merchant=self.other_merchant,
            name='他商户客户',
            mobile='13800138012',
        )
        self.printing_order = printing_models.PrintingOrder.objects.create(
            merchant=self.merchant,
            customer=self.customer,
            fabric='测试面料',
            width='150cm',
            created_by=self.user,
        )
        self.other_printing_order = printing_models.PrintingOrder.objects.create(
            merchant=self.other_merchant,
            customer=self.other_customer,
            fabric='其他面料',
            width='160cm',
            created_by=self.other_user,
        )
        self.content_type = ContentType.objects.get_for_model(printing_models.PrintingOrder)

        self.assignment_published = mes_models.ProductionAssignment.objects.create(
            merchant=self.merchant,
            device=self.device_a,
            content_type=self.content_type,
            object_id=self.printing_order.id,
            assigner=self.employee,
            assignee=self.employee,
            production_quantity=300,
            status=mes_models.ProductionAssignmentStatusEnum.PUBLISHED,
            created_by=self.user,
            operator=self.employee,
        )
        self.assignment_draft = mes_models.ProductionAssignment.objects.create(
            merchant=self.merchant,
            device=self.device_b,
            content_type=self.content_type,
            object_id=self.printing_order.id,
            assigner=self.employee,
            production_quantity=100,
            status=mes_models.ProductionAssignmentStatusEnum.DRAFT,
            created_by=self.user,
            operator=self.employee,
        )
        self.other_assignment = mes_models.ProductionAssignment.objects.create(
            merchant=self.other_merchant,
            device=self.other_device,
            content_type=self.content_type,
            object_id=self.other_printing_order.id,
            assigner=self.other_employee,
            production_quantity=50,
            status=mes_models.ProductionAssignmentStatusEnum.PUBLISHED,
            created_by=self.other_user,
            operator=self.other_employee,
        )

        mes_models.ProductionAssignment.objects.filter(id=self.assignment_published.id).update(
            created_at=timezone.make_aware(datetime.datetime(2026, 5, 1, 10, 0, 0))
        )
        mes_models.ProductionAssignment.objects.filter(id=self.assignment_draft.id).update(
            created_at=timezone.make_aware(datetime.datetime(2026, 5, 3, 12, 0, 0))
        )
        mes_models.ProductionAssignment.objects.filter(id=self.other_assignment.id).update(
            created_at=timezone.make_aware(datetime.datetime(2026, 5, 2, 9, 0, 0))
        )

    def _headers(self, key='agent-test-key'):
        return {'HTTP_AUTHORIZATION': key}

    def test_list_mes_devices_success(self):
        response = self.client.get(
            self.device_list_url,
            {'merchant_id': self.merchant.id},
            **self._headers(),
        )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 2)
        self.assertEqual(response.data['results'][0]['category']['name'], '打印机')

    def test_list_mes_devices_isolates_by_merchant(self):
        response = self.client.get(
            self.device_list_url,
            {'merchant_id': self.other_merchant.id},
            **self._headers(),
        )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 1)
        self.assertEqual(response.data['results'][0]['id'], self.other_device.id)

    def test_list_mes_devices_requires_access_key(self):
        response = self.client.get(self.device_list_url, {'merchant_id': self.merchant.id})
        self.assertEqual(response.status_code, 401)

    def test_list_mes_production_assignments_filters_by_date_range(self):
        response = self.client.get(
            self.assignment_list_url,
            {
                'merchant_id': self.merchant.id,
                'start_date': '2026-05-01',
                'end_date': '2026-05-02',
            },
            **self._headers(),
        )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 1)
        self.assertEqual(response.data['results'][0]['id'], self.assignment_published.id)
        self.assertEqual(response.data['results'][0]['device']['id'], self.device_a.id)

    def test_list_mes_production_assignments_supports_status_and_device_filter(self):
        response = self.client.get(
            self.assignment_list_url,
            {
                'merchant_id': self.merchant.id,
                'start_date': '2026-05-01',
                'end_date': '2026-05-31',
                'device_id': self.device_b.id,
                'status': mes_models.ProductionAssignmentStatusEnum.DRAFT,
            },
            **self._headers(),
        )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['count'], 1)
        self.assertEqual(response.data['results'][0]['id'], self.assignment_draft.id)

    def test_list_mes_production_assignments_rejects_invalid_date_range(self):
        response = self.client.get(
            self.assignment_list_url,
            {
                'merchant_id': self.merchant.id,
                'start_date': '2026-05-31',
                'end_date': '2026-05-01',
            },
            **self._headers(),
        )

        self.assertEqual(response.status_code, 400)
        self.assertIn('end_date', response.data)

    def test_list_mes_production_assignments_requires_access_key(self):
        response = self.client.get(
            self.assignment_list_url,
            {
                'merchant_id': self.merchant.id,
                'start_date': '2026-05-01',
                'end_date': '2026-05-31',
            },
        )
        self.assertEqual(response.status_code, 401)
