import logging
from unittest.mock import MagicMock, patch

import pytest

from pygui.backend.controllers.device_controller import DeviceController
from pygui.backend.data.local_settings import LocalSettings
from pygui.serialcomm.serial_port import WaferInfo


@pytest.fixture
def mock_settings():
    s = LocalSettings()
    s.save_data_dir = ""
    s.connection_status = "Disconnected"
    s.activity_log = []
    s.last_wafer_info = {}
    return s

@pytest.fixture
def controller(mock_settings):
    import shutil
    import tempfile
    temp_dir = tempfile.mkdtemp()
    yield DeviceController(mock_settings, temp_dir)
    shutil.rmtree(temp_dir)

def test_refresh_ports(controller):
    controller._service.enumerate_ports = MagicMock(return_value=["COM1", "COM2"])
    controller.refreshPorts()
    assert "COM1" in controller.availablePorts

def test_detect_wafer_spawns_thread(controller):
    with patch("threading.Thread") as mock_thread:
        info = WaferInfo(
            family_code="P",
            serial_number="P00001",
            sensor_count=244,
            mfg_date_hex="12345678",
            sensor_assigned_hex="01",
            locked_hex="00",
            runtime=100,
            cycle_count=10
        )
        controller._service.detect_all_ports = MagicMock(
            return_value=("COM1", info)
        )
        controller.detectWafer()
        mock_thread.assert_called_once()
        assert controller.operationInProgress

def test_read_memory_without_detect_return_error(controller):
    # License the wafer so this test exercises the "no port" branch rather
    # than short-circuiting on the license guard.
    controller._license_lookup = lambda s: "02"
    controller._last_wafer_info = {"serialNumber": "A00001"}

    emitted_result = None
    def on_read_result(result):
        nonlocal emitted_result
        emitted_result = result

    controller.readResult.connect(on_read_result)
    controller.readMemoryAsync()
    assert emitted_result is not None
    assert "error" in emitted_result

def test_setters_and_properties(controller):
    controller.setSelectedPort("COM3")
    assert controller.selectedPort == "COM3"

    controller.setSaveDataDir("some/custom/dir")
    assert controller.saveDataDir == "some/custom/dir"

    assert controller.connectionStatus == "Disconnected"
    assert controller.dataRowCount == 0
    assert controller.dataColCount == 0
    assert isinstance(controller.lastWaferInfo, list)
    assert controller.dataModel is not None
    assert controller.graphView is not None

def test_clear_activity_log(controller):
    controller._activity_log.append("[12:00:00] Some message")
    assert len(controller._activity_log) > 0
    controller.clearActivityLog()
    assert len(controller._activity_log) == 0

def test_append_activity_log_appends_to_activity_log(controller):
    controller.appendActivityLog("Review trial expired")
    assert "Review trial expired" in controller.activityLog

def test_clear_session(controller):
    controller._connection_status = "Connected"
    controller._selected_port = "COM1"
    controller._last_wafer_info = {"serialNumber": "P00001"}

    controller.clearSession()
    assert controller.connectionStatus == "Disconnected"
    assert controller.selectedPort == ""
    assert controller.lastWaferInfo == ["", "", 0, "", 0, 0]

def test_erase_memory_spawns_thread(controller):
    # License the wafer so the new license guard doesn't block the erase.
    controller._license_lookup = lambda s: "02"
    controller._last_wafer_info = {"serialNumber": "A00001"}
    with patch("threading.Thread") as mock_thread:
        controller.eraseMemory("COM1")
        mock_thread.assert_called_once()
        assert controller.operationInProgress

def test_erase_memory_handler(controller):
    emitted_result = None
    def on_erase_result(result):
        nonlocal emitted_result
        emitted_result = result

    controller.eraseResult.connect(on_erase_result)
    controller._handle_erase_finished(True)
    assert emitted_result == {"success": True}
    assert controller.connectionStatus == "Connected"
    assert not controller.operationInProgress

def test_read_debug_spawns_thread(controller):
    # License the wafer so the license guard doesn't block the debug read.
    controller._license_lookup = lambda s: "02"
    controller._last_wafer_info = {"serialNumber": "A00001"}
    with patch("threading.Thread") as mock_thread:
        controller.readDebug("COM1")
        mock_thread.assert_called_once()
        assert controller.operationInProgress

def test_read_debug_denied_without_license(controller):
    """readDebug() returns D1 sensor data, so it obeys the same license guard."""
    controller._license_lookup = lambda s: ""
    controller._last_wafer_info = {"serialNumber": "A00001"}

    emitted_result = None
    def on_debug_result(result):
        nonlocal emitted_result
        emitted_result = result
    controller.debugResult.connect(on_debug_result)

    with patch("threading.Thread") as mock_thread:
        controller.readDebug("COM1")
        mock_thread.assert_not_called()

    assert emitted_result == {"success": False, "error": "Wafer not licensed"}
    assert not controller.operationInProgress

def test_read_debug_handler(controller):
    emitted_result = None
    def on_debug_result(result):
        nonlocal emitted_result
        emitted_result = result

    controller.debugResult.connect(on_debug_result)
    controller._handle_debug_finished({
        "success": True,
        "sensor_bytes": 512,
        "debug_bytes": 512
    })
    assert emitted_result is not None
    assert emitted_result.get("success") is True
    assert emitted_result.get("sensor_bytes") == 512
    assert emitted_result.get("debug_bytes") == 512
    assert controller.connectionStatus == "Connected"
    assert not controller.operationInProgress

def _make_p_family_bytes(value: int = 0x0100) -> bytes:
    """Synthetic single-block P-family binary: 244 valid words + 12 overhead."""
    data = bytearray()
    for i in range(256):
        word = value if i < 244 else 0
        data += word.to_bytes(2, byteorder="little")
    return bytes(data)


def test_debug_worker_decodes_and_saves_csv(controller, tmp_path):
    controller._last_wafer_info = {"familyCode": "P"}
    controller._save_data_dir = str(tmp_path)
    controller._service.read_wafer_data = MagicMock(
        side_effect=[_make_p_family_bytes(), _make_p_family_bytes()]
    )

    captured = {}
    controller._debugFinished.connect(captured.update)

    controller._debug_worker("COM1")

    assert captured.get("success") is True
    assert captured.get("sensor_bytes") == 512
    assert captured.get("debug_bytes") == 512
    assert "csv_path" in captured
    from pathlib import Path
    assert Path(captured["csv_path"]).exists()
    assert Path(captured["csv_path"]).name == "debug_info.csv"


def test_debug_worker_reports_error_when_csv_save_fails(controller, tmp_path):
    controller._last_wafer_info = {"familyCode": "P"}
    # Point save dir somewhere save_debug_csv cannot write to, so it returns None.
    controller._save_data_dir = str(tmp_path / "debug_info.csv")  # a file, not a dir
    (tmp_path / "debug_info.csv").write_text("occupied")
    controller._service.read_wafer_data = MagicMock(
        side_effect=[_make_p_family_bytes(), _make_p_family_bytes()]
    )

    captured = {}
    controller._debugFinished.connect(captured.update)

    controller._debug_worker("COM1")

    assert captured.get("success") is not True
    assert "error" in captured


def test_debug_handler_logs_csv_path(controller):
    controller._handle_debug_finished({
        "success": True,
        "sensor_bytes": 512,
        "debug_bytes": 512,
        "csv_path": "/tmp/debug_info.csv",
    })
    assert "/tmp/debug_info.csv" in controller.activityLog


def test_parse_and_save_data_and_get_chart_data(controller):
    res = controller.getChartData()
    assert res == {"success": False}

    controller._raw_bytes = bytes([12] * 512)
    emitted_result = None
    def on_parsed(result):
        nonlocal emitted_result
        emitted_result = result

    controller.parsedDataReady.connect(on_parsed)
    controller.parseAndSaveData("P", "COM1")

    assert emitted_result is not None
    assert emitted_result.get("success") is True
    assert controller.dataRowCount == 1
    assert controller.dataColCount == 48

    chart_res = controller.getChartData()
    assert chart_res.get("success") is True
    assert "Sensor1" in chart_res.get("sensor_names")
    assert len(chart_res.get("series")) == 48

def test_logging_handler(controller):
    logger = logging.getLogger("test_logger")
    logger.info("Hello QML Activity Log")
    assert any("Hello QML Activity Log" in line for line in controller._activity_log)

def test_fresh_session_on_startup(mock_settings):
    import shutil
    import tempfile

    # Configure mock settings with some data to simulate previous session
    mock_settings.activity_log = ["[12:00:00] Old message"]
    mock_settings.selected_port = "COM9"
    mock_settings.last_wafer_info = {"familyCode": "P", "serialNumber": "P00002"}
    mock_settings.data_row_count = 50
    mock_settings.data_col_count = 244

    temp_dir = tempfile.mkdtemp()
    try:
        new_controller = DeviceController(mock_settings, temp_dir)
        # Should start completely fresh (empty/default) ignoring the persisted status
        assert len(new_controller._activity_log) == 0
        assert new_controller.selectedPort == ""
        assert new_controller.lastWaferInfo == ["", "", 0, "", 0, 0]
        assert new_controller.dataRowCount == 0
        assert new_controller.dataColCount == 0
    finally:
        shutil.rmtree(temp_dir)

def test_detect_wafer_clears_old_session(controller):
    controller._selected_port = "COM1"
    controller._last_wafer_info = {"familyCode": "P", "serialNumber": "P00001"}
    controller._data_row_count = 10
    controller._data_col_count = 244
    controller._activity_log.append("[12:00:00] Previous message")

    # We mock detect_all_ports to not run long or fail
    controller._service.detect_all_ports = MagicMock(return_value=(None, None))

    with patch("threading.Thread"):
        controller.detectWafer()

        # Verify it cleared all session variables and logs immediately at start of detect
        assert controller.selectedPort == ""
        assert controller.lastWaferInfo == ["", "", 0, "", 0, 0]
        assert controller.dataRowCount == 0
        assert controller.dataColCount == 0

        # Log should only contain "Scanning all ports for wafer ..."
        assert len(controller._activity_log) == 1
        assert "Scanning all ports" in controller._activity_log[0]

def test_device_service_port_caching(controller):
    service = controller._service
    # Mock comports list
    with patch("serial.tools.list_ports.comports", return_value=[]), \
         patch("subprocess.run") as mock_run:
        # Mock subprocess to return empty
        mock_run.return_value.returncode = 0
        mock_run.return_value.stdout = ""

        # Clear cache first
        service.clear_port_scan_cache()

        # First scan should invoke list_ports and subprocess
        ports1 = service.enumerate_ports()
        assert mock_run.call_count == 1

        # Second scan within 2 seconds should return cached list without invoking subprocess
        ports2 = service.enumerate_ports()
        assert mock_run.call_count == 1
        assert ports1 == ports2

        # Clear cache and scan again should invoke subprocess
        service.clear_port_scan_cache()
        service.enumerate_ports()
        assert mock_run.call_count == 2



def test_export_dir_created_under_save_data_dir(controller):
    from pathlib import Path

    result = controller.exportDir()

    assert result == str(Path(controller.saveDataDir) / "Export")
    assert Path(result).is_dir()


def test_wafer_licensed_default_no_license(controller):
    """Default license_lookup (empty lambda) -> waferLicensed() is False."""
    controller._last_wafer_info = {"serialNumber": "P00001"}
    assert controller.waferLicensed() is False


def test_wafer_licensed_with_valid_license(mock_settings):
    """A license_lookup returning a level -> waferLicensed() is True."""
    import shutil
    import tempfile

    temp_dir = tempfile.mkdtemp()
    try:
        controller = DeviceController(
            mock_settings, temp_dir, license_lookup=lambda s: "02"
        )
        controller._last_wafer_info = {"serialNumber": "A00001"}
        assert controller.waferLicensed() is True
    finally:
        shutil.rmtree(temp_dir)


def test_read_memory_denied_without_license(controller):
    """readMemoryAsync() must refuse to spawn a thread for an unlicensed wafer."""
    controller._license_lookup = lambda s: ""
    controller._last_wafer_info = {"serialNumber": "A00001"}
    controller._selected_port = "COM1"

    emitted_result = None
    def on_read_result(result):
        nonlocal emitted_result
        emitted_result = result
    controller.readResult.connect(on_read_result)

    with patch("threading.Thread") as mock_thread:
        controller.readMemoryAsync()
        mock_thread.assert_not_called()

    assert emitted_result is not None
    assert "error" in emitted_result
    assert not controller.operationInProgress


def test_erase_memory_denied_without_license(controller):
    """eraseMemory() must refuse to spawn a thread for an unlicensed wafer."""
    controller._license_lookup = lambda s: ""
    controller._last_wafer_info = {"serialNumber": "A00001"}

    emitted_result = None
    def on_erase_result(result):
        nonlocal emitted_result
        emitted_result = result
    controller.eraseResult.connect(on_erase_result)

    with patch("threading.Thread") as mock_thread:
        controller.eraseMemory("COM1")
        mock_thread.assert_not_called()

    assert emitted_result == {"success": False}
    assert not controller.operationInProgress


def test_read_memory_allowed_with_license(controller):
    """readMemoryAsync() proceeds and spawns a thread once the wafer is licensed."""
    controller._license_lookup = lambda s: "02"
    controller._last_wafer_info = {"serialNumber": "A00001"}
    controller._selected_port = "COM1"

    with patch("threading.Thread") as mock_thread:
        controller.readMemoryAsync()
        mock_thread.assert_called_once()
        assert controller.operationInProgress


def test_erase_memory_allowed_with_license(controller):
    """eraseMemory() proceeds and spawns a thread once the wafer is licensed."""
    controller._license_lookup = lambda s: "02"
    controller._last_wafer_info = {"serialNumber": "A00001"}

    with patch("threading.Thread") as mock_thread:
        controller.eraseMemory("COM1")
        mock_thread.assert_called_once()
        assert controller.operationInProgress


def test_wafer_runtime_exceeded_false_under_threshold(controller):
    controller._last_wafer_info = {"runtime": 100}
    assert controller.waferRuntimeExceeded() is False


def test_wafer_runtime_exceeded_true_over_threshold(controller):
    controller._last_wafer_info = {"runtime": 999}
    assert controller.waferRuntimeExceeded() is True


def test_detect_finished_warns_on_runtime_exceeded(controller):
    info = WaferInfo(
        family_code="P",
        serial_number="P00001",
        sensor_count=244,
        mfg_date_hex="12345678",
        sensor_assigned_hex="01",
        locked_hex="00",
        runtime=999,
        cycle_count=10,
    )
    log_messages = []
    controller.activityLogUpdated.connect(lambda msg: log_messages.append(msg))

    controller._handle_detect_finished("COM1", info)

    assert any("exceeds recommended" in msg for msg in log_messages)


def test_detect_finished_no_warning_under_threshold(controller):
    info = WaferInfo(
        family_code="P",
        serial_number="P00001",
        sensor_count=244,
        mfg_date_hex="12345678",
        sensor_assigned_hex="01",
        locked_hex="00",
        runtime=100,
        cycle_count=10,
    )
    log_messages = []
    controller.activityLogUpdated.connect(lambda msg: log_messages.append(msg))

    controller._handle_detect_finished("COM1", info)

    assert not any("exceeds recommended" in msg for msg in log_messages)
