"""Tests that captures have the expected metadata."""

import json
from datetime import datetime
from pathlib import Path

import piexif
import pytest
from PIL import Image

import labthings_fastapi as lt
from labthings_fastapi.testing import create_thing_without_server

from openflexure_microscope_server.things.background_detect import ChannelDeviationLUV
from openflexure_microscope_server.things.camera import (
    picamera_tuning_file_utils as tf_utils,
)
from openflexure_microscope_server.things.camera.simulation import SimulatedCamera
from openflexure_microscope_server.things.stage.dummy import DummyStage

from ..shared_utils.lt_test_utils import LabThingsTestEnv


@pytest.fixture
def temp_jpeg(tmp_path: Path) -> Path:
    """Create a temporary blank JPEG image."""
    jpeg_path = tmp_path / "test.jpg"
    image = Image.new("RGB", (10, 10), color="white")
    image.save(jpeg_path, "jpeg")
    return jpeg_path


def test_add_metadata_to_capture(temp_jpeg):
    """Use a SimulatedCamera to add metadata to a tmp capture and test fields."""
    cam = create_thing_without_server(SimulatedCamera)
    metadata = {
        "Dummy1": 1,
        "Dummy2": "two",
    }

    current_time = datetime.now()
    capture_metadata = {
        "capture_time": current_time.timestamp(),
        "timezone": current_time.astimezone().utcoffset(),
        "make": "OpenFlexure",
        "model": "OpenFlexure Microscope",
        "things_states": metadata,
    }

    cam._add_metadata_to_capture(str(temp_jpeg), capture_metadata)

    # Reload EXIF
    exif_dict = piexif.load(str(temp_jpeg))

    # Assert UserComment
    user_comment_raw = exif_dict["Exif"][piexif.ExifIFD.UserComment]
    assert json.loads(user_comment_raw.decode("utf-8")) == metadata

    # Assert timestamps
    expected_time_str = datetime.fromtimestamp(
        capture_metadata["capture_time"]
    ).strftime("%Y:%m:%d %H:%M:%S")

    assert (
        exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal].decode() == expected_time_str
    )

    assert (
        exif_dict["Exif"][piexif.ExifIFD.DateTimeDigitized].decode()
        == expected_time_str
    )

    assert exif_dict["0th"][piexif.ImageIFD.DateTime].decode() == expected_time_str

    # Assert timezone offset
    offset_original = exif_dict["Exif"][piexif.ExifIFD.OffsetTimeOriginal].decode()
    offset_digitized = exif_dict["Exif"][piexif.ExifIFD.OffsetTimeDigitized].decode()

    tz = capture_metadata["timezone"]
    hours = int(tz.total_seconds() // 3600)
    minutes = int((abs(tz.total_seconds()) % 3600) // 60)
    sign = "+" if hours >= 0 else "-"
    expected_offset = f"{sign}{abs(hours):02d}:{minutes:02d}"

    assert offset_original == expected_offset
    assert offset_digitized == expected_offset

    # Assert Make and Model
    assert exif_dict["0th"][piexif.ImageIFD.Make].decode() == capture_metadata["make"]
    assert exif_dict["0th"][piexif.ImageIFD.Model].decode() == capture_metadata["model"]


@pytest.fixture
def test_env() -> LabThingsTestEnv:
    """Yield a test environment with the Simulated Camera and Dummy Stage."""
    thing_conf = {
        "camera": SimulatedCamera,
        "stage": DummyStage,
        "bg_channel_deviations_luv": ChannelDeviationLUV,
    }
    with LabThingsTestEnv(things=thing_conf) as env:
        yield env


@pytest.fixture
def camera(test_env) -> lt.Thing:
    """Return the SimulatedCamera Thing set up in the test environment."""
    return test_env.get_thing_by_type(SimulatedCamera)


def test_add_metadata_to_simulated_capture(camera, temp_jpeg):
    """Test metadata from simulated camera includes expected camera_board: simulator."""
    metadata = camera.thing_state
    current_time = datetime.now()
    capture_metadata = {
        "capture_time": current_time.timestamp(),
        "timezone": current_time.astimezone().utcoffset(),
        "make": "OpenFlexure",
        "model": "OpenFlexure Microscope",
        "things_states": metadata,
    }

    camera._add_metadata_to_capture(str(temp_jpeg), capture_metadata)

    # Reload EXIF
    exif_dict = piexif.load(str(temp_jpeg))

    # Assert UserComment
    user_comment_raw = exif_dict["Exif"][piexif.ExifIFD.UserComment]
    assert json.loads(user_comment_raw.decode("utf-8")) == metadata
    assert json.loads(user_comment_raw.decode("utf-8"))["camera"] == "SimulatedCamera"


def test_picamera_adds_metadata(mock_picam_thing):
    """Test PiCamera adds camera_board and tuning metadata."""
    camera = mock_picam_thing

    # Inject controlled values
    camera._camera_board = "imx219"
    camera.exposure_time = 1234
    camera.colour_gains = (1.1, 1.2)
    camera.analogue_gain = 2.5
    camera.tuning = tf_utils.set_gamma_curve(camera.tuning, [0, 0, 5, 50])

    state = camera.thing_state

    # Assert metadata has been set
    assert state["camera_board"] == "imx219"
    assert state["tuning"] == {
        "exposure_time": 1234,
        "colour_gains": (1.1, 1.2),
        "analogue_gain": 2.5,
        "gamma_correction": [0, 0, 5, 50],
    }


def test_picamera_metadata_written_to_exif(mock_picam_thing, temp_jpeg, mocker):
    """Ensure PiCamera metadata is written into JPEG EXIF."""
    camera = mock_picam_thing

    camera._camera_board = "imx219"
    camera.exposure_time = 1234
    camera.colour_gains = (1.1, 1.2)
    camera.analogue_gain = 2.5
    camera.tuning = tf_utils.set_gamma_curve(camera.tuning, [0, 0, 5, 50])

    # Mock the server interface to return the camera's own state
    mock_interface = mocker.Mock()
    mock_interface.get_thing_states.return_value = camera.thing_state
    camera._thing_server_interface = mock_interface

    capture_metadata = camera._capture_metadata()

    camera._add_metadata_to_capture(str(temp_jpeg), capture_metadata)

    exif_dict = piexif.load(str(temp_jpeg))
    user_comment = json.loads(exif_dict["Exif"][piexif.ExifIFD.UserComment].decode())

    assert user_comment["camera"] == "PiCameraV2"
    assert user_comment["camera_board"] == "imx219"
    # gamma_correction keys are cast to strings
    assert user_comment["tuning"] == {
        "exposure_time": 1234,
        "colour_gains": [1.1, 1.2],
        "analogue_gain": 2.5,
        "gamma_correction": [0, 0, 5, 50],
    }
