import time
from typing import Any, Callable

import numpy as np
import pytest

# -----------------------------------------------------------------------------
# Configuration
# -----------------------------------------------------------------------------

PREFIX = "Test:Bridge:"
TIMEOUT_DEFAULT = 20.0
TIMEOUT_RECOVERY = 50.0
POLL_INTERVAL = 0.1

# -----------------------------------------------------------------------------
# Domain Helper Class (Encapsulation)
# -----------------------------------------------------------------------------


class BridgeController:
    """
    Encapsulates interaction with the Bridge/IOC.
    Acts as a 'driver' for the device under test to keep tests clean.
    """

    def __init__(self, uth: Any, prefix: str = PREFIX):
        self.uth = uth
        self.prefix = prefix
        self.echo_prefix = f"{prefix}Echo-"
        self.pva_prefix = f"{prefix}PVA-"

    def wait_for_condition(
        self,
        pv: str,
        condition: Callable[[Any], bool],
        timeout: float = TIMEOUT_DEFAULT,
        msg: str = "Timeout",
    ) -> Any:
        """Polls a PV until condition is met."""
        start = time.time()
        last_val = None

        while time.time() - start < timeout:
            try:
                val = self.uth.pvget(pv)
                if val is not None:
                    last_val = val
                    if condition(val):
                        return val
            except Exception:
                pass
            time.sleep(POLL_INTERVAL)

        raise TimeoutError(f"{msg} | PV: {pv} | Last Value: {last_val}")

    def run_task_cycle(
        self, timeout: float = 30.0, assert_success: bool = True
    ) -> None:
        """Triggers task and ensures full Busy=1 -> Busy=0 cycle."""
        # 1. Trigger
        self.uth.pvput(f"{self.prefix}Trigger", 1)

        # 2. Wait for Start (Idle -> Busy)
        self.wait_for_condition(
            f"{self.prefix}Busy",
            lambda v: v == 1,
            timeout=timeout,
            msg="Task failed to start (Busy flag never went high)",
        )

        # 3. Wait for Finish (Busy -> Idle)
        self.wait_for_condition(
            f"{self.prefix}Busy",
            lambda v: v == 0,
            timeout=timeout,
            msg="Task failed to finish (Busy flag stuck high)",
        )

        if assert_success:
            assert self.uth.pvget(f"{self.prefix}TaskStatus") == 0

    def set_pva_image(self, data: np.ndarray) -> None:
        """Helper to flatten and write image data to PVA structure."""
        height, width = data.shape
        # Note: Depending on backend,
        # dimensions might need to be reversed in metadata vs data
        self.uth.pvput(f"{self.pva_prefix}ArraySizeX", width)
        self.uth.pvput(f"{self.pva_prefix}ArraySizeY", height)
        self.uth.pvput(f"{self.pva_prefix}ArrayData.NELM", width * height)
        self.uth.pvput(f"{self.pva_prefix}ArrayData", data.ravel())

    def wait_for_daemon_death(
        self, daemon_process, timeout: float = TIMEOUT_RECOVERY
    ) -> None:
        start = time.time()
        while time.time() - start < timeout:
            if not daemon_process.is_running():
                return
            time.sleep(1)
        raise TimeoutError(f"Daemon failed to exit after {timeout}s")


# -----------------------------------------------------------------------------
# Fixtures
# -----------------------------------------------------------------------------


@pytest.fixture
def bridge(uth):
    """Provides an initialized BridgeController."""
    return BridgeController(uth)


# -----------------------------------------------------------------------------
# Logic & Scalar Tests
# -----------------------------------------------------------------------------


def test_update_task_duration(bridge, daemon_process):
    """Tests that task duration is updated."""
    bridge.run_task_cycle()
    time.sleep(10)
    assert bridge.uth.pvget(f"{bridge.prefix}TaskDuration") > 2


@pytest.mark.parametrize(
    "input_suffix, result_suffix, input_val, expected_val",
    [
        ("InputFloat", "ResultFloat", 10.0, 25.0),  # Standard Float
        ("InputFloat", "ResultFloat", 0.0, 0.0),  # Zero Float
        ("InputFloat", "ResultFloat", -4.0, -10.0),  # Negative Float
        ("InputInt", "ResultInt", 5, 105),  # Standard Int
        ("InputInt", "ResultInt", -50, 50),  # Negative Int
        ("InputStr", "ResultStr", "World", "Echo: World"),  # Standard Str
        ("InputStr", "ResultStr", "", "Echo: "),  # Empty Str
        ("InputStr", "ResultStr", "!@#$", "Echo: !@#$"),  # Special Char Str
    ],
)
def test_scalar_processing(
    bridge, daemon_process, input_suffix, result_suffix, input_val, expected_val
):
    """Tests basic scalar processing logic (Float, Int, String)."""
    bridge.uth.pvput(f"{bridge.echo_prefix}{input_suffix}", input_val)
    bridge.run_task_cycle()
    assert bridge.uth.pvget(f"{bridge.echo_prefix}{result_suffix}") == expected_val


def test_process_enum_cycle(bridge, daemon_process):
    """Verifies enum wrapping cycle (0->1, 1->2, 2->0)."""
    for inp, expected in [(0, 1), (1, 2), (2, 0)]:
        bridge.uth.pvput(f"{bridge.echo_prefix}InputEnum", inp)
        bridge.run_task_cycle()
        assert bridge.uth.pvget(f"{bridge.echo_prefix}ResultEnum") == expected


@pytest.mark.parametrize("daemon_process", [True], indirect=True)
def test_trigger_override_logic(bridge, daemon_process):
    """Verifies custom trigger logic specific to daemon configuration 'Y'."""
    bridge.run_task_cycle()
    assert bridge.uth.pvget(f"{bridge.echo_prefix}ResultInt") == 9999
    assert bridge.uth.pvget(f"{bridge.echo_prefix}ResultStr") == "reset_cycle"
    # Ensure trigger resets
    assert bridge.uth.pvget(f"{bridge.prefix}Trigger") == 0


# -----------------------------------------------------------------------------
# Array & Image Tests
# -----------------------------------------------------------------------------


def test_process_array(bridge, daemon_process):
    """
    Array Logic: Output = Input * 2
    """
    # Case 1: Standard Array
    input_arr = np.array([1.0, 2.5, -3.0])
    bridge.uth.pvput(f"{bridge.echo_prefix}InputArr", input_arr)
    bridge.run_task_cycle()

    res = bridge.uth.pvget(f"{bridge.echo_prefix}ResultArr")
    # Check only first N elements to ignore potential trailing zeros in PV buffer
    assert np.allclose(res[: len(input_arr)], input_arr * 2)

    # Case 2: Full Capacity (Assuming NELM=100)
    full_arr = np.ones(100) * 5.0
    bridge.uth.pvput(f"{bridge.echo_prefix}InputArr", full_arr)
    bridge.run_task_cycle()
    assert np.allclose(bridge.uth.pvget(f"{bridge.echo_prefix}ResultArr"), full_arr * 2)


@pytest.mark.parametrize(
    "shape_gen",
    [
        lambda: (512, 512),  # Standard Square
        lambda: (2, 512),  # High Aspect Ratio Strip
        lambda: (768, 1024),  # Max Capacity
    ],
)
def test_image_processing_shapes(bridge, daemon_process, shape_gen):
    """Verifies image processing with various shapes."""
    shape = shape_gen()
    # Create random data within uint8 range, but stored as int64 for the test
    data = np.random.randint(0, 100, size=shape, dtype=np.int64)

    bridge.set_pva_image(data)
    bridge.run_task_cycle()

    result = bridge.uth.pvget(f"{bridge.pva_prefix}ArrayData")
    assert np.array_equal(result, data.ravel() * 10), f"Mismatch for shape {shape}"


def test_image_negative_values(bridge, daemon_process):
    """Verifies signed integer handling in images."""
    data = np.array([[-10, 20], [-50, 0]], dtype=np.int64)
    bridge.set_pva_image(data)
    bridge.run_task_cycle()

    result = bridge.uth.pvget(f"{bridge.pva_prefix}ArrayData")
    assert np.array_equal(result, data.ravel() * 10)


# -----------------------------------------------------------------------------
# Lifecycle & Recovery Tests
# -----------------------------------------------------------------------------


def test_heartbeat_lifecycle(bridge, daemon_process):
    """Verifies that the daemon is alive and updating heartbeat."""
    initial = bridge.uth.pvget(f"{bridge.prefix}Heartbeat")
    bridge.wait_for_condition(
        f"{bridge.prefix}Heartbeat",
        lambda v: v != initial,
        timeout=5.0,
        msg="Heartbeat PV did not toggle",
    )


def test_daemon_recover_after_ioc_crash(bridge, daemon_process, uth):
    """
    Verifies daemon survives intermittent IOC outages.
    """
    assert uth.ioc.is_running()
    bridge.uth.pvput(f"{bridge.prefix}Busy", 0)

    for i in range(5):
        print(f"--- Outage Cycle {i+1} ---")

        # 1. Kill IOC
        uth.stop_ioc()
        assert not uth.ioc.is_running()

        # 2. Wait a moment to ensure daemon attempts (and fails) communication
        time.sleep(2)
        assert daemon_process.is_running(), "Daemon died prematurely"

        # 3. Restart IOC
        uth.ioc = uth.start_ioc()
        time.sleep(5)

        # 4. Wait for reconnection implicitly by trying to run a task
        # If the daemon is stuck, run_task_cycle will timeout
        try:
            bridge.run_task_cycle()
        except TimeoutError:
            pytest.fail(f"Daemon failed to recover processing after outage cycle {i+1}")

        assert bridge.uth.pvget(f"{bridge.prefix}Trigger") == 0


def test_daemon_suicide_and_restart(bridge, daemon_process, uth):
    """
    Simulates production failure: IOC Crash -> Daemon Suicide -> Supervisor Restart.
    """
    assert uth.ioc.is_running()

    print("Killing IOC to induce suicide pact...")
    uth.stop_ioc()

    # Wait for daemon to detect failure and exit
    bridge.wait_for_daemon_death(daemon_process)
    print("Daemon successfully exited.")

    # Simulate Supervisor Restart
    uth.ioc = uth.start_ioc()
    daemon_process.start()

    # Allow brief startup time for P4P context
    time.sleep(2)

    print("Verifying system recovery...")
    bridge.uth.pvput(f"{bridge.prefix}Busy", 0)

    # Verify processing works on new instances
    bridge.run_task_cycle()
    assert bridge.uth.pvget(f"{bridge.prefix}Trigger") == 0
    print("System fully recovered.")


def test_daemon_restart_stress(bridge, daemon_process, uth):
    """
    Stress test: Repeatedly restarting the daemon while IOC stays up.
    """
    for _ in range(5):
        daemon_process.stop()
        assert not daemon_process.is_running()

        daemon_process.start()
        # Wait for heartbeat to prove it's alive before running task
        bridge.wait_for_condition(
            f"{bridge.prefix}Heartbeat", lambda v: True, timeout=5.0
        )

        bridge.run_task_cycle()
        assert bridge.uth.pvget(f"{bridge.prefix}Trigger") == 0
