from __future__ import annotations

import unittest
from datetime import date, datetime

from fastapi import HTTPException
from sqlalchemy import BigInteger, create_engine, select
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import Session, sessionmaker


@compiles(BigInteger, "sqlite")
def _compile_bigint_for_sqlite(type_, compiler, **kw) -> str:
    _ = type_, compiler, kw
    return "INTEGER"


import app.models.master_data  # noqa: E402,F401
import app.models.miniapp  # noqa: E402,F401
import app.models.operations  # noqa: E402,F401
import app.models.org  # noqa: E402,F401
import app.models.planning  # noqa: E402,F401
import app.models.sales  # noqa: E402,F401
from app.api.routes import production as production_routes  # noqa: E402
from app.api.routes.production import _miniapp_report_item_rows, list_miniapp_operation_reports  # noqa: E402
from app.models.base import Base  # noqa: E402
from app.models.miniapp import MiniAppPersonnel, MiniAppProductionReport, MiniAppProductionReportItem  # noqa: E402
from app.models.operations import OperationReport  # noqa: E402
from app.models.org import SystemConfig  # noqa: E402
from app.services.system_config import (  # noqa: E402
    MINIAPP_OPERATION_REPORT_LAST_SYNC_CONFIG_CODE,
    format_miniapp_operation_report_last_sync,
    get_miniapp_operation_report_last_sync,
    parse_miniapp_operation_report_last_sync,
    upsert_miniapp_operation_report_last_sync,
)


class MiniAppOperationReportDateFilterTest(unittest.TestCase):
    def setUp(self) -> None:
        engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
        Base.metadata.create_all(engine)
        self.SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
        self.db: Session = self.SessionLocal()
        self._seed_reports()

    def tearDown(self) -> None:
        self.db.close()

    def _seed_reports(self) -> None:
        now = datetime(2026, 6, 1, 8, 0, 0)
        self.db.add(MiniAppPersonnel(phone="13800000000", name="测试员工", created_at=now, updated_at=now))
        for report_id, report_date in [(1, date(2026, 5, 10)), (2, date(2026, 5, 20)), (3, date(2026, 6, 1))]:
            self.db.add(
                MiniAppProductionReport(
                    id=report_id,
                    attendance_point_name="测试考勤点",
                    session_id=report_id,
                    employee_phone="13800000000",
                    report_date=report_date,
                    start_at=datetime.combine(report_date, now.time()),
                    end_at=datetime.combine(report_date, now.time()),
                    duration_minutes=0,
                    break_minutes=0,
                    effective_minutes=0,
                    total_good_qty=0,
                    total_output_qty=0,
                    actual_beat=0,
                    standard_beat=0,
                    expected_workload=0,
                    pace_rate=0,
                    workload_rate=0,
                    status="approved",
                    submitted_at=datetime.combine(report_date, now.time()),
                    updated_at=datetime.combine(report_date, now.time()),
                )
            )
            self.db.add(
                MiniAppProductionReportItem(
                    id=report_id,
                    report_id=report_id,
                    attendance_point_name="测试考勤点",
                    device_no="1#",
                    project_no="P-001",
                    product_name=f"测试产品{report_id}",
                    raw_material_batch_no=f"JH-RM-{report_id:04d}",
                    process_name="1序",
                    standard_beat=0,
                    standard_workload=0,
                    good_qty=10,
                    defect_qty=0,
                    scrap_qty=0,
                    allocated_minutes=0,
                    started_at=datetime.combine(report_date, now.time()),
                    created_at=datetime.combine(report_date, now.time()),
                )
            )
        self.db.commit()

    def test_miniapp_report_item_rows_filters_by_report_date_range(self) -> None:
        rows = _miniapp_report_item_rows(
            self.db,
            limit=100,
            start_date=date(2026, 5, 18),
            end_date=date(2026, 6, 1),
        )

        self.assertEqual([row["source_item_id"] for row in rows], [3, 2])
        self.assertEqual({row["report_date"] for row in rows}, {date(2026, 5, 20), date(2026, 6, 1)})

    def test_miniapp_operation_reports_rejects_start_date_after_end_date(self) -> None:
        with self.assertRaises(HTTPException) as exc:
            list_miniapp_operation_reports(
                limit=100,
                sync_to_erp=False,
                start_date=date(2026, 6, 1),
                end_date=date(2026, 5, 18),
                db=self.db,
            )

        self.assertEqual(exc.exception.status_code, 400)
        self.assertIn("开始日期不能晚于结束日期", exc.exception.detail)

    def test_miniapp_operation_report_last_sync_config_round_trips_datetime(self) -> None:
        timestamp = datetime(2026, 6, 11, 10, 30, 15)

        row = upsert_miniapp_operation_report_last_sync(self.db, timestamp, updated_by=1)
        self.db.commit()

        persisted = self.db.scalar(
            select(SystemConfig).where(SystemConfig.config_code == MINIAPP_OPERATION_REPORT_LAST_SYNC_CONFIG_CODE)
        )
        self.assertIsNotNone(persisted)
        self.assertEqual(row.config_value, "2026-06-11 10:30:15")
        self.assertEqual(format_miniapp_operation_report_last_sync(timestamp), "2026-06-11 10:30:15")
        self.assertEqual(parse_miniapp_operation_report_last_sync("2026-06-11 10:30:15"), timestamp)
        self.assertEqual(get_miniapp_operation_report_last_sync(self.db), timestamp)

    def test_list_reports_with_sync_disabled_does_not_call_sync(self) -> None:
        calls: list[int] = []
        original_sync = production_routes._sync_miniapp_item_to_erp

        def fake_sync(db, source_item_id, **kwargs):
            calls.append(source_item_id)
            return None

        production_routes._sync_miniapp_item_to_erp = fake_sync
        try:
            rows = list_miniapp_operation_reports(
                limit=100,
                sync_to_erp=False,
                start_date=date(2026, 5, 18),
                end_date=date(2026, 6, 1),
                db=self.db,
            )
        finally:
            production_routes._sync_miniapp_item_to_erp = original_sync

        self.assertEqual(len(rows), 2)
        self.assertEqual(calls, [])
        self.assertEqual(self.db.query(OperationReport).count(), 0)

    def test_list_reports_with_sync_disabled_skips_erp_target_resolution(self) -> None:
        calls: list[str] = []
        original_find_ledger = production_routes._find_production_ledger_for_miniapp_item
        original_find_work_order = production_routes._find_work_order_for_miniapp_item
        original_find_operation = production_routes._find_work_order_operation_for_miniapp_item

        def fake_find_ledger(*args, **kwargs):
            calls.append("production_ledger")
            return None

        def fake_find_work_order(*args, **kwargs):
            calls.append("work_order")
            return None

        def fake_find_operation(*args, **kwargs):
            calls.append("operation")
            return None

        production_routes._find_production_ledger_for_miniapp_item = fake_find_ledger
        production_routes._find_work_order_for_miniapp_item = fake_find_work_order
        production_routes._find_work_order_operation_for_miniapp_item = fake_find_operation
        try:
            rows = list_miniapp_operation_reports(
                limit=100,
                sync_to_erp=False,
                start_date=date(2026, 5, 18),
                end_date=date(2026, 6, 1),
                db=self.db,
            )
        finally:
            production_routes._find_production_ledger_for_miniapp_item = original_find_ledger
            production_routes._find_work_order_for_miniapp_item = original_find_work_order
            production_routes._find_work_order_operation_for_miniapp_item = original_find_operation

        self.assertEqual(len(rows), 2)
        self.assertEqual(calls, [])

    def test_list_reports_caches_missing_erp_reports_without_per_row_scalar_lookup(self) -> None:
        original_scalar = self.db.scalar

        def fail_scalar(*args, **kwargs):
            raise AssertionError("list refresh must not query missing ERP reports one row at a time")

        rows = _miniapp_report_item_rows(
            self.db,
            limit=100,
            start_date=date(2026, 5, 18),
            end_date=date(2026, 6, 1),
        )
        cache = production_routes._new_miniapp_resolve_cache()
        report_nos = [
            production_routes._miniapp_report_no(int(row["source_report_id"]), int(row["source_item_id"]))
            for row in rows
        ]
        production_routes._preload_miniapp_operation_reports(self.db, report_nos, cache)

        self.db.scalar = fail_scalar
        try:
            result = [
                production_routes._build_miniapp_operation_report_read(
                    self.db,
                    row,
                    None,
                    cache=cache,
                    resolve_targets=False,
                )
                for row in rows
            ]
        finally:
            self.db.scalar = original_scalar

        self.assertEqual(len(result), 2)
        self.assertTrue(all(not row.is_synced_to_erp for row in result))

    def test_sync_reports_uses_last_success_time_not_page_filter(self) -> None:
        upsert_miniapp_operation_report_last_sync(self.db, datetime(2026, 5, 19, 0, 0, 0), updated_by=1)
        self.db.commit()
        calls: list[int] = []
        original_sync = production_routes._sync_miniapp_item_to_erp
        original_now = production_routes.datetime

        class FixedDateTime(datetime):
            @classmethod
            def now(cls, tz=None):
                return cls(2026, 6, 11, 12, 0, 0)

        def fake_sync(db, source_item_id, **kwargs):
            calls.append(source_item_id)
            return None

        production_routes._sync_miniapp_item_to_erp = fake_sync
        production_routes.datetime = FixedDateTime
        try:
            result = production_routes.sync_miniapp_operation_reports(db=self.db)
        finally:
            production_routes._sync_miniapp_item_to_erp = original_sync
            production_routes.datetime = original_now

        self.assertEqual(calls, [2, 3])
        self.assertEqual(result.total, 2)
        self.assertEqual(result.sync_from, datetime(2026, 5, 19, 0, 0, 0))
        self.assertEqual(result.sync_to, datetime(2026, 6, 11, 12, 0, 0))
        self.assertEqual(get_miniapp_operation_report_last_sync(self.db), datetime(2026, 6, 11, 12, 0, 0))

    def test_sync_state_returns_last_success_time(self) -> None:
        timestamp = datetime(2026, 6, 10, 18, 20, 30)
        upsert_miniapp_operation_report_last_sync(self.db, timestamp, updated_by=1)
        self.db.commit()

        result = production_routes.get_miniapp_operation_report_sync_state(db=self.db)

        self.assertEqual(result.last_synced_at, timestamp)


if __name__ == "__main__":
    unittest.main()
