from io import StringIO
from unittest.mock import patch

from django.core.management import call_command
from django.conf import settings
from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone

from api_v1 import models as api_models
from api_v1.tasks import sync_external_printing_records
from basic_info import models as basic_models
from printing import models as printing_models
from stateflow.models import Process, State


class ExternalPrintingRecordsSyncTaskTest(TestCase):
    def setUp(self):
        self.merchant = basic_models.Merchant.objects.create(
            name='测试印染厂',
            type=basic_models.MerchantTypeEnum.FACTORY,
        )
        self.sync_user = User.objects.create_user(username='sync-user', password='testpass')
        self.sync_employee = basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.sync_user,
            name='同步用户',
        )
        self.operator_user = User.objects.create_user(username='operator-user', password='testpass')
        self.operator_employee = basic_models.Employee.objects.create(
            merchant=self.merchant,
            sys_user=self.operator_user,
            name='钰涵',
        )
        self.customer = basic_models.Customer.objects.create(
            merchant=self.merchant,
            name='鸿烨服饰',
        )
        self.category = basic_models.ProductCategory.objects.create(
            merchant=self.merchant,
            name='同步产品类',
            product_prefix='TP',
        )
        self.existing_product = basic_models.Product.objects.create(
            merchant=self.merchant,
            category=self.category,
            name='Tj1712#12号色-24码',
            image='product_images/existing.jpg',
        )

        state = State.objects.create(name='待生产')
        self.process = Process.objects.create(name='默认印染流程')
        self.process.replace_nodes([state])

        settings.PRINTING_DEFAULT_PROCESS_ID = self.process.id
        settings.PRINTING_EXTERNAL_SYNC_USER_ID = self.sync_user.id
        settings.PRINTING_EXTERNAL_SYNC_PRODUCT_CATEGORY_ID = self.category.id

    def _build_payload(self, records):
        return {
            'mode': 'incremental',
            'cursor_before': 10,
            'cursor_after': 12,
            'last_record_id': records[-1]['ID'],
            'updated': False,
            'count': len(records),
            'records': records,
        }

    def _build_record(self, *, record_id: int, product_name: str):
        return {
            'BeiZhu': '手感一定要柔软',
            'BeiZhuC': '10件',
            'BianHaoID': 'KD20432358',
            'BianHaoKD': '1.27',
            'CaoZY': '钰涵',
            'FidJ': r'\\fw\\2026年-LWQ15\\2026\\H鸿烨\\Tj1712#',
            'HpName': '120克本白四面弹单定',
            'ID': record_id,
            'JiJiaDW': '/段',
            'KdRiQi': '2026-01-26T20:00:52Z',
            'KhID': 'KH00999',
            'MeoA': 'LWQ15',
            'RiQi': '2026-01-26T00:00:00Z',
            'SHDZ': '客户布  烧花',
            'SeHao': '1.51',
            'ShuLiang': '10.00',
            'ShuLiangZ': '2.68',
            'SubID': 1,
            'YanSe': product_name,
            'area': '周边',
            'customer': {
                'KhID': 'KH00999',
                'KhName': '鸿烨服饰',
            },
        }

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_existing_product_skips_image_fetch(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        mock_fetch_records.return_value = self._build_payload(
            [self._build_record(record_id=1000000, product_name=self.existing_product.name)]
        )
        mock_advance_cursor.return_value = {'updated': True}

        result = sync_external_printing_records.run(limit=100)

        self.assertEqual(result['jobs_created'], 1)
        self.assertEqual(result['failed_records'], 0)
        self.assertEqual(mock_fetch_image.call_count, 0)
        mock_advance_cursor.assert_called_once_with(cursor_value=1000000)

        job = printing_models.PrintingJob.objects.get(original_id=1000000)
        self.assertEqual(job.product, self.existing_product)
        self.assertEqual(job.sub_id, 1)
        self.assertIsNone(job.external_product_name)

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._upload_product_image')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_missing_product_creates_product_and_job(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_upload_product_image,
        mock_advance_cursor,
    ):
        new_product_name = 'Tj1712#12号色-25码'
        mock_fetch_records.return_value = self._build_payload(
            [self._build_record(record_id=1000001, product_name=new_product_name)]
        )
        mock_fetch_image.return_value = {
            'bytes': b'fake-image',
            'content_type': 'image/jpeg',
            'name': 'abc#123.jpg',
        }
        mock_advance_cursor.return_value = {'updated': True}

        def _fake_upload(product, image_payload):
            product.image = 'product_images/generated.jpg'
            product.save(update_fields=['image'])

        mock_upload_product_image.side_effect = _fake_upload

        result = sync_external_printing_records.run(limit=100)

        self.assertEqual(result['jobs_created'], 1)
        self.assertEqual(result['failed_records'], 0)
        mock_fetch_image.assert_called_once_with(new_product_name)

        product = basic_models.Product.objects.get(name=new_product_name, merchant=self.merchant)
        self.assertIsNotNone(product)
        self.assertEqual(product.category, self.category)

        job = printing_models.PrintingJob.objects.get(original_id=1000001)
        self.assertEqual(job.product, product)
        self.assertEqual(job.printing_order.position, r'\\fw\\2026年-LWQ15\\2026\\H鸿烨\\Tj1712#')
        self.assertEqual(job.printing_order.fabric, '120克本白四面弹单定')
        self.assertEqual(job.printing_order.area, '周边')

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_order_position_uses_first_record_fidj(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        first_record = self._build_record(record_id=1000000, product_name=self.existing_product.name)
        second_record = self._build_record(record_id=1000001, product_name=self.existing_product.name)
        first_record['FidJ'] = r'\\fw\\path\\first'
        second_record['FidJ'] = r'\\fw\\path\\second'
        mock_fetch_records.return_value = self._build_payload([first_record, second_record])
        mock_advance_cursor.return_value = {'updated': True}

        result = sync_external_printing_records.run(limit=100)

        self.assertEqual(result['orders_created'], 1)
        self.assertEqual(result['jobs_created'], 1)
        self.assertEqual(result['jobs_updated'], 1)
        self.assertEqual(mock_fetch_image.call_count, 0)

        order = printing_models.PrintingOrder.objects.get(external_order_id='KD20432358')
        self.assertEqual(order.position, r'\\fw\\path\\first')
        self.assertEqual(order.fabric, '120克本白四面弹单定')
        self.assertEqual(order.area, '周边')
        self.assertEqual(printing_models.PrintingJob.objects.count(), 1)

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_partial_failure_records_external_record_id_and_advances_cursor(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        success_record = self._build_record(record_id=1000000, product_name=self.existing_product.name)
        failed_record = self._build_record(record_id=1000001, product_name='不存在图片的产品#1')
        mock_fetch_records.return_value = self._build_payload([success_record, failed_record])
        mock_advance_cursor.return_value = {'updated': True}

        def _fake_fetch_image(product_name):
            raise RuntimeError(f'外部图片不存在: {product_name}')

        mock_fetch_image.side_effect = _fake_fetch_image

        result = sync_external_printing_records.run(limit=100)

        self.assertEqual(result['jobs_created'], 1)
        self.assertEqual(result['failed_records'], 1)
        self.assertEqual(result['failed_record_ids'], [1000001])
        mock_advance_cursor.assert_called_once_with(cursor_value=1000001)

        self.assertTrue(
            printing_models.PrintingJob.objects.filter(original_id=1000000).exists()
        )
        self.assertFalse(
            printing_models.PrintingJob.objects.filter(original_id=1000001).exists()
        )

        failure = api_models.PrintingExternalSyncFailure.objects.get(
            external_record_id=1000001
        )
        self.assertEqual(failure.external_order_id, 'KD20432358')
        self.assertIn('外部图片不存在', failure.error)

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_sync_can_skip_advancing_external_cursor(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        mock_fetch_records.return_value = self._build_payload(
            [self._build_record(record_id=1000100, product_name=self.existing_product.name)]
        )

        result = sync_external_printing_records.run(limit=100, advance_cursor=False)

        self.assertEqual(result['jobs_created'], 1)
        self.assertEqual(result['failed_records'], 0)
        self.assertFalse(result['cursor_updated'])
        self.assertFalse(result['advance_cursor'])
        mock_advance_cursor.assert_not_called()

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_missing_white_fabric_image_does_not_block_sync_or_record_failure(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        product_name = '加厚白布-25码'
        mock_fetch_records.return_value = self._build_payload(
            [self._build_record(record_id=1000101, product_name=product_name)]
        )
        mock_advance_cursor.return_value = {'updated': True}
        mock_fetch_image.side_effect = RuntimeError(f'外部图片不存在: {product_name}')

        result = sync_external_printing_records.run(limit=100)

        self.assertEqual(result['jobs_created'], 1)
        self.assertEqual(result['failed_records'], 0)
        self.assertFalse(api_models.PrintingExternalSyncFailure.objects.exists())

        product = basic_models.Product.objects.get(merchant=self.merchant, name=product_name)
        self.assertFalse(product.image)
        job = printing_models.PrintingJob.objects.get(original_id=1000101)
        self.assertEqual(job.product, product)

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_existing_non_white_product_without_image_still_records_failure_when_image_missing(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        product = basic_models.Product.objects.create(
            merchant=self.merchant,
            category=self.category,
            name='Tj1712#12号色-无图',
        )
        mock_fetch_records.return_value = self._build_payload(
            [self._build_record(record_id=1000102, product_name=product.name)]
        )
        mock_advance_cursor.return_value = {'updated': True}
        mock_fetch_image.side_effect = RuntimeError(f'外部图片不存在: {product.name}')

        result = sync_external_printing_records.run(limit=100)

        self.assertEqual(result['jobs_created'], 0)
        self.assertEqual(result['failed_records'], 1)
        self.assertEqual(result['failed_record_ids'], [1000102])
        self.assertFalse(printing_models.PrintingJob.objects.filter(original_id=1000102).exists())

        failure = api_models.PrintingExternalSyncFailure.objects.get(external_record_id=1000102)
        self.assertEqual(failure.product_name, product.name)
        self.assertIn('外部图片不存在', failure.error)

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_missing_customer_creates_customer_and_syncs_successfully(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        missing_customer_name = '新补建客户'
        record = self._build_record(record_id=1000002, product_name=self.existing_product.name)
        record['customer']['KhName'] = missing_customer_name
        record['KhID'] = 'KH01000'
        mock_fetch_records.return_value = self._build_payload([record])
        mock_advance_cursor.return_value = {'updated': True}

        result = sync_external_printing_records.run(limit=100)

        self.assertEqual(result['failed_records'], 0)
        self.assertEqual(result['jobs_created'], 1)
        self.assertEqual(mock_fetch_image.call_count, 0)

        customer = basic_models.Customer.objects.get(merchant=self.merchant, name=missing_customer_name)
        self.assertEqual(customer.created_by, self.sync_employee)

        order = printing_models.PrintingOrder.objects.get(external_order_id='KD20432358')
        self.assertEqual(order.customer, customer)
        self.assertEqual(order.external_customer_name, missing_customer_name)

    def test_retry_command_replays_failure_raw_and_clears_failure_rows(self):
        missing_customer_name = '重试补建客户'
        raw_record = self._build_record(record_id=1000003, product_name=self.existing_product.name)
        raw_record['customer']['KhName'] = missing_customer_name
        raw_record['KhID'] = 'KH01001'
        api_models.PrintingExternalSyncFailure.objects.create(
            run_date=timezone.localdate(),
            external_record_id=1000003,
            external_order_id='KD20432358',
            product_name=self.existing_product.name,
            error=f'未找到客户: {missing_customer_name}',
            raw=raw_record,
            attempts=1,
            last_attempt_at=timezone.now(),
        )

        output = StringIO()
        call_command(
            'retry_external_printing_sync_failures',
            '--record-id',
            '1000003',
            stdout=output,
        )

        self.assertTrue(
            basic_models.Customer.objects.filter(merchant=self.merchant, name=missing_customer_name).exists()
        )
        self.assertTrue(
            printing_models.PrintingJob.objects.filter(original_id=1000003).exists()
        )
        self.assertFalse(
            api_models.PrintingExternalSyncFailure.objects.filter(external_record_id=1000003).exists()
        )
        self.assertIn("'retried_records': 1", output.getvalue())
        self.assertIn("'limit': None", output.getvalue())
        self.assertIn("'remaining_failures': 0", output.getvalue())

    def test_retry_command_without_limit_retries_all_failures(self):
        first_record = self._build_record(record_id=1000010, product_name=self.existing_product.name)
        first_record['customer']['KhName'] = '补建客户甲'
        second_record = self._build_record(record_id=1000011, product_name=self.existing_product.name)
        second_record['customer']['KhName'] = '补建客户乙'

        for record in [first_record, second_record]:
            api_models.PrintingExternalSyncFailure.objects.create(
                run_date=timezone.localdate(),
                external_record_id=record['ID'],
                external_order_id=record['BianHaoID'],
                product_name=record['YanSe'],
                error=f"未找到客户: {record['customer']['KhName']}",
                raw=record,
                attempts=1,
                last_attempt_at=timezone.now(),
            )

        output = StringIO()
        call_command('retry_external_printing_sync_failures', stdout=output)

        self.assertIn("'limit': None", output.getvalue())
        self.assertIn("'retried_records': 2", output.getvalue())
        self.assertIn("'remaining_failures': 0", output.getvalue())
        self.assertFalse(api_models.PrintingExternalSyncFailure.objects.exists())

    @patch('api_v1.management.commands.sync_external_printing_records.sync_external_printing_records')
    def test_sync_command_supports_no_advance_cursor(self, mock_task):
        mock_task.run.return_value = {'cursor_updated': False, 'advance_cursor': False}

        output = StringIO()
        call_command('sync_external_printing_records', '--limit', '5', '--no-advance-cursor', stdout=output)

        mock_task.run.assert_called_once_with(limit=5, advance_cursor=False)
        self.assertIn("'advance_cursor': False", output.getvalue())

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_same_order_and_product_overwrites_existing_job_instead_of_creating_new_one(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        initial_record = self._build_record(record_id=1000000, product_name=self.existing_product.name)
        updated_record = self._build_record(record_id=1000001, product_name=self.existing_product.name)
        updated_record['ShuLiang'] = '20.00'
        updated_record['ShuLiangZ'] = '3.15'
        updated_record['BeiZhuC'] = '20件'
        updated_record['SubID'] = 2
        mock_fetch_image.return_value = {'bytes': b'', 'content_type': 'image/jpeg', 'name': 'unused.jpg'}
        mock_advance_cursor.return_value = {'updated': True}

        mock_fetch_records.return_value = self._build_payload([initial_record])
        first_result = sync_external_printing_records.run(limit=100)

        mock_fetch_records.return_value = self._build_payload([updated_record])
        second_result = sync_external_printing_records.run(limit=100)

        self.assertEqual(first_result['jobs_created'], 1)
        self.assertEqual(second_result['jobs_created'], 0)
        self.assertEqual(second_result['jobs_updated'], 1)
        self.assertEqual(mock_fetch_image.call_count, 0)

        jobs = printing_models.PrintingJob.objects.all()
        self.assertEqual(jobs.count(), 1)
        job = jobs.get()
        self.assertEqual(job.original_id, 1000001)
        self.assertEqual(job.sub_id, 2)
        self.assertEqual(job.quantity, 20)
        self.assertEqual(job.size, '3.15')
        self.assertEqual(job.pieces, 20)
        self.assertEqual(job.printing_order.fabric, '120克本白四面弹单定')

    @patch('api_v1.tasks._advance_external_printing_cursor')
    @patch('api_v1.tasks._fetch_external_product_image')
    @patch('api_v1.tasks._fetch_external_printing_records')
    def test_order_area_updates_and_defaults_to_empty_string(
        self,
        mock_fetch_records,
        mock_fetch_image,
        mock_advance_cursor,
    ):
        initial_record = self._build_record(record_id=1000012, product_name=self.existing_product.name)
        updated_record = self._build_record(record_id=1000013, product_name=self.existing_product.name)
        updated_record['area'] = ''
        mock_advance_cursor.return_value = {'updated': True}

        mock_fetch_records.return_value = self._build_payload([initial_record])
        first_result = sync_external_printing_records.run(limit=100)

        mock_fetch_records.return_value = self._build_payload([updated_record])
        second_result = sync_external_printing_records.run(limit=100)

        self.assertEqual(first_result['orders_created'], 1)
        self.assertEqual(second_result['orders_updated'], 1)
        self.assertEqual(mock_fetch_image.call_count, 0)

        order = printing_models.PrintingOrder.objects.get(external_order_id='KD20432358')
        self.assertEqual(order.area, '')
