"""Test the server without creating a full HTTP server and socket connection.

Rather than spinning up a full uvicorn webserver for each test these tests use
the FastAPI ``TestClient`` or directly communicate with the underlying
LabThings-FastAPI code. This increases speed of testing significantly.

For tests that require a full running server see the ``lifecycle_test``
directory in the tests directory.

This file used to be called test_dummy_server when it was in the unit_test suite, but
it has been moved as it artificaially inflated coverage.
"""

import json

import numpy as np
import piexif
import pytest
from PIL import Image

import labthings_fastapi as lt


def test_autofocus(simulation_test_env):
    """Test Fast Autofocus can run doesn't raise an exception."""
    stage = simulation_test_env.get_thing_client("stage")
    autofocus = simulation_test_env.get_thing_client("autofocus")
    assert stage.position["z"] == 0
    # Autofocus 5 times and check each ends within 500 steps
    for i in range(5):
        autofocus.fast_autofocus()
        assert abs(stage.position["z"]) < 500, f"Autofocus failed on iteration {i}"


def test_grab_jpeg(simulation_test_env):
    """Check that grab_jpeg returns a blob that can be opened."""
    camera = simulation_test_env.get_thing_client("camera")
    blob = camera.grab_jpeg()
    image = Image.open(blob.open())
    assert image.size == (820, 616)


@pytest.mark.parametrize("fmt", ["jpeg", "png"])
def test_capture_and_metadata(simulation_test_env, fmt):
    """Check that the position is encoded into the image metadata."""
    camera = simulation_test_env.get_thing_client("camera")
    blob = getattr(camera, f"capture_{fmt}")()
    image = Image.open(blob.open())
    exif_dict = piexif.load(image.info["exif"])
    encoded_metadata = exif_dict["Exif"][piexif.ExifIFD.UserComment]
    metadata = json.loads(encoded_metadata)
    assert "position" in metadata["stage"]
    assert metadata["stage"]["position"] == {"x": 0, "y": 0, "z": 0}
    assert image.size == (820, 616)


def test_stage(simulation_test_env):
    """Test moving the stage forwards and backwards."""
    stage = simulation_test_env.get_thing_client("stage")
    start = stage.position
    move = {"x": 1, "y": 2, "z": 3}
    move_back = {"x": -1, "y": -2, "z": -3}
    stage.move_relative(**move)
    pos = stage.position

    assert start["x"] + move["x"] == pos["x"]
    assert start["y"] + move["y"] == pos["y"]
    assert start["z"] + move["z"] == pos["z"]

    # Move back
    stage.move_relative(**move_back)
    pos = stage.position

    # Check nack at start.
    assert start["x"] == pos["x"]
    assert start["y"] == pos["y"]
    assert start["z"] == pos["z"]


def test_capture_array(simulation_test_env):
    """Capture array from simulation and check the size is as expected."""
    camera = simulation_test_env.get_thing_client("camera")
    array = np.asarray(camera.capture_array())
    assert array.shape == (616, 820, 3)


def test_camera_stage_mapping_calibration(simulation_test_env):
    """Check that camera stage mapping runs and returns the expected result."""
    camera = simulation_test_env.get_thing_client("camera")
    # Remove camera settling time for speed.
    camera.settling_time = 0
    csm = simulation_test_env.get_thing_client("camera_stage_mapping")
    stage = simulation_test_env.get_thing_client("stage")

    # Check it starts uncalibrated
    assert csm.calibration_required
    assert csm.image_to_stage_displacement_matrix is None
    assert csm.last_calibration is None
    # And therefore that actions error
    with pytest.raises(lt.exceptions.FailedToInvokeActionError):
        csm.move_in_image_coordinates(x=10)

    # Calibrate
    csm.calibrate_xy()

    assert not csm.calibration_required
    assert csm.image_to_stage_displacement_matrix is not None
    assert isinstance(csm.last_calibration, dict)
    # Check it returned home
    assert stage.position == {"x": 0, "y": 0, "z": 0}
    csm.move_in_image_coordinates(x=10, y=0)
    assert stage.position == {"x": -5, "y": 0, "z": 0}
    csm_matrix = csm.image_to_stage_displacement_matrix
    expected_matrix = [[0, 0.5], [-0.5, 0]]
    assert isinstance(csm_matrix, list)
    # Check CSM is as expected to an absolute tolerance of 1e-3
    assert np.allclose(csm_matrix, expected_matrix, atol=1e-3)


def test_measure_settling_time(simulation_test_env):
    """Test measure_settling_time captures data while moving and holding."""
    autofocus = simulation_test_env.get_thing_client("autofocus")
    stage = simulation_test_env.get_thing_client("stage")

    start_z = stage.position["z"]

    delay = 1
    dz = 1000

    data = autofocus.measure_settling_time(delay=delay, dz=dz)

    # Check returned arrays contain data
    assert len(data["jpeg_times"]) > 0
    assert len(data["jpeg_sizes"]) > 0
    assert len(data["stage_times"]) == 4
    assert len(data["stage_positions"]) == 4

    # Check the stage moved down then back up
    z_positions = [p["z"] for p in data["stage_positions"]]

    assert z_positions[0] == start_z
    assert z_positions[1] == start_z - dz
    assert z_positions[2] == start_z - dz
    assert z_positions[3] == start_z

    # Check final stage position returned to start
    assert stage.position["z"] == start_z

    # Check frames continued to be collected after the final move completed
    final_move_time = data["stage_times"][-1]
    jpeg_times_after_final_move = [t for t in data["jpeg_times"] if t > final_move_time]

    assert len(jpeg_times_after_final_move) > 0


def test_measure_settling_time_hold_duration(simulation_test_env):
    """Test measure_settling_time continues capturing during hold."""
    autofocus = simulation_test_env.get_thing_client("autofocus")

    short_data = autofocus.measure_settling_time(delay=0, dz=400)
    long_data = autofocus.measure_settling_time(delay=1, dz=400)

    # Longer delay should collect more JPEG frames
    assert len(long_data["jpeg_times"]) > len(short_data["jpeg_times"])
    assert len(long_data["jpeg_sizes"]) > len(short_data["jpeg_sizes"])
