# -*- coding: utf-8 -*-
"""Service tests for mid-section defect analysis."""

from __future__ import annotations

from unittest.mock import patch

import pandas as pd

from mes_dashboard.services.mid_section_defect_service import (
    _attribute_materials,
    _attribute_wafer_roots,
    _build_detail_table,
    build_trace_aggregation_from_events,
    export_csv,
    query_analysis,
    query_analysis_detail,
    query_all_loss_reasons,
    query_station_options,
)


def test_query_analysis_invalid_date_format_returns_error():
    result = query_analysis('2025/01/01', '2025-01-31')

    assert 'error' in result
    assert 'YYYY-MM-DD' in result['error']


def test_query_analysis_start_after_end_returns_error():
    result = query_analysis('2025-02-01', '2025-01-31')

    assert 'error' in result
    assert '起始日期不能晚於結束日期' in result['error']


def test_query_analysis_exceeds_max_days_returns_error():
    result = query_analysis('2025-01-01', '2025-12-31')

    assert 'error' in result
    assert '180' in result['error']


@patch('mes_dashboard.services.mid_section_defect_service.query_analysis')
def test_query_analysis_detail_returns_sorted_first_page(mock_query_analysis):
    mock_query_analysis.return_value = {
        'detail': [
            {'CONTAINERNAME': 'C', 'DEFECT_RATE': 0.3},
            {'CONTAINERNAME': 'A', 'DEFECT_RATE': 5.2},
            {'CONTAINERNAME': 'B', 'DEFECT_RATE': 3.1},
        ]
    }

    result = query_analysis_detail('2025-01-01', '2025-01-31', page=1, page_size=2)

    assert [row['CONTAINERNAME'] for row in result['detail']] == ['A', 'B']
    assert result['pagination'] == {
        'page': 1,
        'page_size': 2,
        'total_count': 3,
        'total_pages': 2,
    }


@patch('mes_dashboard.services.mid_section_defect_service.query_analysis')
def test_query_analysis_detail_clamps_page_to_last_page(mock_query_analysis):
    mock_query_analysis.return_value = {
        'detail': [
            {'CONTAINERNAME': 'A', 'DEFECT_RATE': 9.9},
            {'CONTAINERNAME': 'B', 'DEFECT_RATE': 8.8},
            {'CONTAINERNAME': 'C', 'DEFECT_RATE': 7.7},
        ]
    }

    result = query_analysis_detail('2025-01-01', '2025-01-31', page=10, page_size=2)

    assert result['pagination']['page'] == 2
    assert result['pagination']['total_pages'] == 2
    assert len(result['detail']) == 1
    assert result['detail'][0]['CONTAINERNAME'] == 'C'


@patch('mes_dashboard.services.mid_section_defect_service.query_analysis')
def test_query_analysis_detail_returns_error_passthrough(mock_query_analysis):
    mock_query_analysis.return_value = {'error': '日期格式無效'}

    result = query_analysis_detail('2025-01-01', '2025-01-31', page=1, page_size=200)

    assert result == {'error': '日期格式無效'}


@patch('mes_dashboard.services.mid_section_defect_service.query_analysis')
def test_query_analysis_detail_returns_none_on_service_failure(mock_query_analysis):
    mock_query_analysis.return_value = None

    result = query_analysis_detail('2025-01-01', '2025-01-31', page=1, page_size=200)

    assert result is None


@patch('mes_dashboard.services.mid_section_defect_service.cache_get')
@patch('mes_dashboard.services.mid_section_defect_service.read_sql_df')
def test_query_all_loss_reasons_cache_hit_skips_query(mock_read_sql_df, mock_cache_get):
    mock_cache_get.return_value = {'loss_reasons': ['Cached_A', 'Cached_B']}

    result = query_all_loss_reasons()

    assert result == {'loss_reasons': ['Cached_A', 'Cached_B']}
    mock_read_sql_df.assert_not_called()


@patch('mes_dashboard.services.mid_section_defect_service.cache_get', return_value=None)
@patch('mes_dashboard.services.mid_section_defect_service.cache_set')
@patch('mes_dashboard.services.mid_section_defect_service.read_sql_df')
@patch('mes_dashboard.services.mid_section_defect_service.SQLLoader.load')
def test_query_all_loss_reasons_cache_miss_queries_and_caches_sorted_values(
    mock_sql_load,
    mock_read_sql_df,
    mock_cache_set,
    _mock_cache_get,
):
    mock_sql_load.return_value = 'SELECT ...'
    mock_read_sql_df.return_value = pd.DataFrame(
        {'LOSSREASONNAME': ['B_REASON', None, 'A_REASON', 'B_REASON']}
    )

    result = query_all_loss_reasons()

    assert result == {'loss_reasons': ['A_REASON', 'B_REASON']}
    mock_cache_set.assert_called_once_with(
        'mid_section_loss_reasons:None:',
        {'loss_reasons': ['A_REASON', 'B_REASON']},
        ttl=86400,
    )


@patch('mes_dashboard.services.mid_section_defect_service.cache_set')
@patch('mes_dashboard.services.mid_section_defect_service.cache_get', return_value=None)
@patch('mes_dashboard.services.mid_section_defect_service.release_lock')
@patch('mes_dashboard.services.mid_section_defect_service.try_acquire_lock', return_value=True)
@patch('mes_dashboard.services.mid_section_defect_service._fetch_upstream_history')
@patch('mes_dashboard.services.mid_section_defect_service._resolve_full_genealogy')
@patch('mes_dashboard.services.mid_section_defect_service._fetch_station_detection_data')
def test_trace_aggregation_matches_query_analysis_summary(
    mock_fetch_detection_data,
    mock_resolve_genealogy,
    mock_fetch_upstream_history,
    _mock_lock,
    _mock_release_lock,
    _mock_cache_get,
    _mock_cache_set,
):
    detection_df = pd.DataFrame([
        {
            'CONTAINERID': 'CID-001',
            'CONTAINERNAME': 'LOT-001',
            'TRACKINQTY': 100,
            'REJECTQTY': 5,
            'LOSSREASONNAME': 'R1',
            'WORKFLOW': 'WF-A',
            'PRODUCTLINENAME': 'PKG-A',
            'PJ_TYPE': 'TYPE-A',
            'DETECTION_EQUIPMENTNAME': 'EQ-01',
            'TRACKINTIMESTAMP': '2025-01-10 10:00:00',
            'FINISHEDRUNCARD': 'FR-001',
        },
        {
            'CONTAINERID': 'CID-002',
            'CONTAINERNAME': 'LOT-002',
            'TRACKINQTY': 120,
            'REJECTQTY': 6,
            'LOSSREASONNAME': 'R2',
            'WORKFLOW': 'WF-B',
            'PRODUCTLINENAME': 'PKG-B',
            'PJ_TYPE': 'TYPE-B',
            'DETECTION_EQUIPMENTNAME': 'EQ-02',
            'TRACKINTIMESTAMP': '2025-01-11 10:00:00',
            'FINISHEDRUNCARD': 'FR-002',
        },
    ])

    ancestors = {
        'CID-001': {'CID-101'},
        'CID-002': set(),
    }
    upstream_normalized = {
        'CID-101': [{
            'workcenter_group': '中段',
            'equipment_id': 'EQ-01',
            'equipment_name': 'EQ-01',
            'spec_name': 'SPEC-A',
            'track_in_time': '2025-01-09 08:00:00',
        }],
        'CID-002': [{
            'workcenter_group': '中段',
            'equipment_id': 'EQ-02',
            'equipment_name': 'EQ-02',
            'spec_name': 'SPEC-B',
            'track_in_time': '2025-01-11 08:00:00',
        }],
    }
    upstream_events = {
        'CID-101': [{
            'WORKCENTER_GROUP': '中段',
            'EQUIPMENTID': 'EQ-01',
            'EQUIPMENTNAME': 'EQ-01',
            'SPECNAME': 'SPEC-A',
            'TRACKINTIMESTAMP': '2025-01-09 08:00:00',
        }],
        'CID-002': [{
            'WORKCENTER_GROUP': '中段',
            'EQUIPMENTID': 'EQ-02',
            'EQUIPMENTNAME': 'EQ-02',
            'SPECNAME': 'SPEC-B',
            'TRACKINTIMESTAMP': '2025-01-11 08:00:00',
        }],
    }

    mock_fetch_detection_data.return_value = detection_df
    mock_resolve_genealogy.return_value = ancestors
    mock_fetch_upstream_history.return_value = upstream_normalized

    summary = query_analysis('2025-01-01', '2025-01-31')
    staged_summary = build_trace_aggregation_from_events(
        '2025-01-01',
        '2025-01-31',
        seed_container_ids=['CID-001', 'CID-002'],
        lineage_ancestors={
            'CID-001': ['CID-101'],
            'CID-002': [],
        },
        upstream_events_by_cid=upstream_events,
    )

    assert staged_summary['available_loss_reasons'] == summary['available_loss_reasons']
    assert staged_summary['genealogy_status'] == summary['genealogy_status']
    assert staged_summary['detail_total_count'] == len(summary['detail'])

    assert staged_summary['kpi']['total_input'] == summary['kpi']['total_input']
    assert staged_summary['kpi']['lot_count'] == summary['kpi']['lot_count']
    assert staged_summary['kpi']['total_defect_qty'] == summary['kpi']['total_defect_qty']
    assert abs(
        staged_summary['kpi']['total_defect_rate'] - summary['kpi']['total_defect_rate']
    ) <= 0.01

    assert staged_summary['daily_trend'] == summary['daily_trend']
    assert staged_summary['charts'].keys() == summary['charts'].keys()


def test_query_station_options_returns_ordered_list():
    result = query_station_options()
    assert isinstance(result, list)
    assert len(result) == 12
    assert result[0]['name'] == '切割'
    assert result[0]['order'] == 0
    assert result[-1]['name'] == '測試'
    assert result[-1]['order'] == 11


# --- _attribute_materials tests ---

def _make_detection_data(entries):
    """Helper: build detection_data dict from simplified entries."""
    data = {}
    for e in entries:
        data[e['cid']] = {
            'containername': e.get('name', e['cid']),
            'trackinqty': e['trackinqty'],
            'rejectqty_by_reason': e.get('reasons', {}),
        }
    return data


def test_attribute_materials_basic_rate_calculation():
    detection_data = _make_detection_data([
        {'cid': 'C1', 'trackinqty': 100, 'reasons': {'R1': 5}},
        {'cid': 'C2', 'trackinqty': 200, 'reasons': {'R1': 10}},
    ])
    ancestors = {'C1': {'A1'}, 'C2': {'A1'}}
    materials_by_cid = {
        'A1': [{'MATERIALPARTNAME': 'PART-A', 'MATERIALLOTNAME': 'LOT-X'}],
    }

    result = _attribute_materials(detection_data, ancestors, materials_by_cid)

    assert len(result) == 1
    assert result[0]['MATERIAL_KEY'] == 'PART-A (LOT-X)'
    assert result[0]['INPUT_QTY'] == 300
    assert result[0]['DEFECT_QTY'] == 15
    assert abs(result[0]['DEFECT_RATE'] - 5.0) < 0.01
    assert result[0]['DETECTION_LOT_COUNT'] == 2


def test_attribute_materials_null_lot_name():
    detection_data = _make_detection_data([
        {'cid': 'C1', 'trackinqty': 100, 'reasons': {'R1': 3}},
    ])
    ancestors = {'C1': {'A1'}}
    materials_by_cid = {
        'A1': [{'MATERIALPARTNAME': 'PART-B', 'MATERIALLOTNAME': None}],
    }

    result = _attribute_materials(detection_data, ancestors, materials_by_cid)

    assert len(result) == 1
    assert result[0]['MATERIAL_KEY'] == 'PART-B'
    assert result[0]['MATERIAL_LOT_NAME'] == ''


def test_attribute_materials_with_loss_reason_filter():
    detection_data = _make_detection_data([
        {'cid': 'C1', 'trackinqty': 100, 'reasons': {'R1': 5, 'R2': 3}},
    ])
    ancestors = {'C1': {'A1'}}
    materials_by_cid = {
        'A1': [{'MATERIALPARTNAME': 'P', 'MATERIALLOTNAME': 'L'}],
    }

    result = _attribute_materials(detection_data, ancestors, materials_by_cid, loss_reasons=['R1'])

    assert result[0]['DEFECT_QTY'] == 5


# --- _attribute_wafer_roots tests ---

def test_attribute_wafer_roots_basic():
    detection_data = _make_detection_data([
        {'cid': 'C1', 'name': 'LOT-1', 'trackinqty': 100, 'reasons': {'R1': 5}},
        {'cid': 'C2', 'name': 'LOT-2', 'trackinqty': 200, 'reasons': {'R1': 10}},
    ])
    roots = {'C1': 'ROOT-A', 'C2': 'ROOT-A'}

    result = _attribute_wafer_roots(detection_data, roots)

    assert len(result) == 1
    assert result[0]['ROOT_CONTAINER_NAME'] == 'ROOT-A'
    assert result[0]['INPUT_QTY'] == 300
    assert result[0]['DEFECT_QTY'] == 15


def test_attribute_wafer_roots_self_root():
    """LOTs with no root mapping should use their own container name."""
    detection_data = _make_detection_data([
        {'cid': 'C1', 'name': 'LOT-SELF', 'trackinqty': 100, 'reasons': {'R1': 2}},
    ])
    roots = {}  # No root for C1

    result = _attribute_wafer_roots(detection_data, roots)

    assert len(result) == 1
    assert result[0]['ROOT_CONTAINER_NAME'] == 'LOT-SELF'


def test_attribute_wafer_roots_multiple_roots():
    detection_data = _make_detection_data([
        {'cid': 'C1', 'name': 'L1', 'trackinqty': 100, 'reasons': {'R1': 5}},
        {'cid': 'C2', 'name': 'L2', 'trackinqty': 200, 'reasons': {'R1': 20}},
    ])
    roots = {'C1': 'ROOT-A', 'C2': 'ROOT-B'}

    result = _attribute_wafer_roots(detection_data, roots)

    assert len(result) == 2
    # Sorted by DEFECT_RATE desc
    assert result[0]['ROOT_CONTAINER_NAME'] == 'ROOT-B'
    assert result[1]['ROOT_CONTAINER_NAME'] == 'ROOT-A'


# --- _build_detail_table tests ---

def _make_detection_df(rows):
    """Helper: build a DataFrame like _fetch_station_detection_data output."""
    return pd.DataFrame(rows)


def test_build_detail_table_structured_upstream_machines():
    """UPSTREAM_MACHINES should be a list of {station, machine} objects."""
    df = _make_detection_df([
        {
            'CONTAINERID': 'C1', 'CONTAINERNAME': 'LOT-1', 'PJ_TYPE': 'T',
            'PRODUCTLINENAME': 'P', 'WORKFLOW': 'W', 'FINISHEDRUNCARD': 'FR',
            'DETECTION_EQUIPMENTNAME': 'DET-01', 'TRACKINQTY': 100,
            'REJECTQTY': 5, 'LOSSREASONNAME': 'R1',
        },
    ])
    ancestors = {'C1': {'A1'}}
    upstream_by_cid = {
        'A1': [
            {'workcenter_group': '中段', 'equipment_name': 'WIRE-01'},
            {'workcenter_group': '後段', 'equipment_name': 'DIE-01'},
        ],
        'C1': [
            {'workcenter_group': '測試', 'equipment_name': 'TEST-01'},
        ],
    }

    result = _build_detail_table(df, ancestors, upstream_by_cid)

    assert len(result) == 1
    row = result[0]
    machines = row['UPSTREAM_MACHINES']
    assert isinstance(machines, list)
    assert len(machines) == 3
    assert {'station': '中段', 'machine': 'WIRE-01'} in machines
    assert {'station': '後段', 'machine': 'DIE-01'} in machines
    assert {'station': '測試', 'machine': 'TEST-01'} in machines
    assert row['UPSTREAM_MACHINE_COUNT'] == 3


def test_build_detail_table_structured_upstream_materials():
    """UPSTREAM_MATERIALS should be a list of {part, lot} objects."""
    df = _make_detection_df([
        {
            'CONTAINERID': 'C1', 'CONTAINERNAME': 'LOT-1', 'PJ_TYPE': 'T',
            'PRODUCTLINENAME': 'P', 'WORKFLOW': 'W', 'FINISHEDRUNCARD': 'FR',
            'DETECTION_EQUIPMENTNAME': 'DET-01', 'TRACKINQTY': 100,
            'REJECTQTY': 0, 'LOSSREASONNAME': '',
        },
    ])
    ancestors = {'C1': {'A1'}}
    upstream_by_cid = {}
    materials_by_cid = {
        'A1': [
            {'MATERIALPARTNAME': 'PART-X', 'MATERIALLOTNAME': 'ML-1'},
            {'MATERIALPARTNAME': 'PART-Y', 'MATERIALLOTNAME': ''},
        ],
    }

    result = _build_detail_table(
        df, ancestors, upstream_by_cid, materials_by_cid=materials_by_cid,
    )

    assert len(result) == 1
    materials = result[0]['UPSTREAM_MATERIALS']
    assert isinstance(materials, list)
    assert len(materials) == 2
    assert {'part': 'PART-X', 'lot': 'ML-1'} in materials
    assert {'part': 'PART-Y', 'lot': ''} in materials


def test_build_detail_table_wafer_root():
    """WAFER_ROOT should be the root ancestor container name."""
    df = _make_detection_df([
        {
            'CONTAINERID': 'C1', 'CONTAINERNAME': 'LOT-1', 'PJ_TYPE': 'T',
            'PRODUCTLINENAME': 'P', 'WORKFLOW': 'W', 'FINISHEDRUNCARD': 'FR',
            'DETECTION_EQUIPMENTNAME': 'D', 'TRACKINQTY': 100,
            'REJECTQTY': 3, 'LOSSREASONNAME': 'R1',
        },
    ])
    ancestors = {'C1': set()}
    upstream_by_cid = {}
    roots = {'C1': 'WAFER-ROOT-001'}

    result = _build_detail_table(
        df, ancestors, upstream_by_cid, roots=roots,
    )

    assert result[0]['WAFER_ROOT'] == 'WAFER-ROOT-001'


def test_build_detail_table_multiple_defect_reasons_expand_rows():
    """LOT with multiple defect reasons should produce one row per reason."""
    df = _make_detection_df([
        {
            'CONTAINERID': 'C1', 'CONTAINERNAME': 'LOT-1', 'PJ_TYPE': 'T',
            'PRODUCTLINENAME': 'P', 'WORKFLOW': 'W', 'FINISHEDRUNCARD': 'FR',
            'DETECTION_EQUIPMENTNAME': 'D', 'TRACKINQTY': 200,
            'REJECTQTY': 5, 'LOSSREASONNAME': 'R1',
        },
        {
            'CONTAINERID': 'C1', 'CONTAINERNAME': 'LOT-1', 'PJ_TYPE': 'T',
            'PRODUCTLINENAME': 'P', 'WORKFLOW': 'W', 'FINISHEDRUNCARD': 'FR',
            'DETECTION_EQUIPMENTNAME': 'D', 'TRACKINQTY': 200,
            'REJECTQTY': 3, 'LOSSREASONNAME': 'R2',
        },
    ])

    result = _build_detail_table(df, {'C1': set()}, {})

    assert len(result) == 2
    reasons = [r['LOSS_REASON'] for r in result]
    assert 'R1' in reasons
    assert 'R2' in reasons
    assert result[0]['DEFECT_QTY'] + result[1]['DEFECT_QTY'] == 8


def test_build_detail_table_deduplicates_machines():
    """Same machine appearing in multiple ancestors should appear only once."""
    df = _make_detection_df([
        {
            'CONTAINERID': 'C1', 'CONTAINERNAME': 'LOT-1', 'PJ_TYPE': 'T',
            'PRODUCTLINENAME': 'P', 'WORKFLOW': 'W', 'FINISHEDRUNCARD': 'FR',
            'DETECTION_EQUIPMENTNAME': 'D', 'TRACKINQTY': 100,
            'REJECTQTY': 1, 'LOSSREASONNAME': 'R1',
        },
    ])
    ancestors = {'C1': {'A1', 'A2'}}
    # Same machine in both ancestors
    upstream_by_cid = {
        'A1': [{'workcenter_group': '中段', 'equipment_name': 'EQ-01'}],
        'A2': [{'workcenter_group': '中段', 'equipment_name': 'EQ-01'}],
    }

    result = _build_detail_table(df, ancestors, upstream_by_cid)

    assert result[0]['UPSTREAM_MACHINE_COUNT'] == 1
    assert len(result[0]['UPSTREAM_MACHINES']) == 1


@patch('mes_dashboard.services.mid_section_defect_service.query_analysis')
def test_export_csv_flattens_structured_fields(mock_query_analysis):
    """CSV export should flatten UPSTREAM_MACHINES and UPSTREAM_MATERIALS to strings."""
    mock_query_analysis.return_value = {
        'detail': [
            {
                'CONTAINERNAME': 'LOT-1',
                'PJ_TYPE': 'T',
                'PRODUCTLINENAME': 'P',
                'WORKFLOW': 'W',
                'FINISHEDRUNCARD': 'FR',
                'DETECTION_EQUIPMENTNAME': 'D',
                'INPUT_QTY': 100,
                'LOSS_REASON': 'R1',
                'DEFECT_QTY': 5,
                'DEFECT_RATE': 5.0,
                'ANCESTOR_COUNT': 1,
                'UPSTREAM_MACHINE_COUNT': 2,
                'UPSTREAM_MACHINES': [
                    {'station': '中段', 'machine': 'WIRE-01'},
                    {'station': '後段', 'machine': 'DIE-02'},
                ],
                'UPSTREAM_MATERIALS': [
                    {'part': 'PART-A', 'lot': 'ML-1'},
                ],
                'WAFER_ROOT': 'ROOT-001',
            },
        ],
    }

    lines = list(export_csv('2025-01-01', '2025-01-31', direction='backward'))

    # First line is BOM, second is header, third is data
    assert len(lines) == 3
    data_line = lines[2]
    assert '中段/WIRE-01, 後段/DIE-02' in data_line
    assert 'PART-A/ML-1' in data_line
