"""Tests for AppackCompiler — Sprint 4."""

from __future__ import annotations

import json
import zipfile
from pathlib import Path

import pytest

from arautopilot.core.actuator_config import ActuatorConfig, ActuatorType
from arautopilot.core.pid_config import PidConfig, PidGains
from arautopilot.core.project_config import ProjectConfig
from arautopilot.core.sensor_config import (
    DualRudderSensorConfig,
    RudderSensorConfig,
    RudderSensorType,
)
from arautopilot.core.vessel_config import VesselConfig, VesselType
from arautopilot.studio.compiler.appack import AppackCompiler


def _make_project(*, dual_sensors: bool = False) -> ProjectConfig:
    redundant = (
        RudderSensorConfig(
            type=RudderSensorType.AS5048A_SPI,
            label="Redundant – actuator arm",
            spi_cs_gpio=11,
        )
        if dual_sensors
        else None
    )
    return ProjectConfig(
        client_name="Test Client",
        project_name="Test Project",
        vessel=VesselConfig(
            name="Test Vessel",
            type=VesselType.YACHT_MOTOR_PLANEO,
            length_m=30.0,
            max_speed_kn=18.0,
            actuator=ActuatorConfig(
                type=ActuatorType.HYDRAULIC_REVERSIBLE,
                max_rudder_angle_deg=35.0,
            ),
            pid=PidConfig(
                inner_loop_base=PidGains(kp=1.5, ki=0.1, kd=0.05),
                outer_loop_base=PidGains(kp=2.0, ki=0.05, kd=0.3),
            ),
            sensors=DualRudderSensorConfig(
                primary=RudderSensorConfig(
                    type=RudderSensorType.AS5048A_SPI,
                    spi_cs_gpio=10,
                ),
                redundant=redundant,
            ),
        ),
    )


class TestAppackCompiler:
    def test_compile_creates_file(self, tmp_path: Path) -> None:
        out = tmp_path / "test.appack"
        compiler = AppackCompiler(_make_project())
        result = compiler.compile(out)
        assert result == out
        assert out.exists()

    def test_appack_is_valid_zip(self, tmp_path: Path) -> None:
        out = tmp_path / "test.appack"
        AppackCompiler(_make_project()).compile(out)
        assert zipfile.is_zipfile(out)

    def test_appack_contains_required_files(self, tmp_path: Path) -> None:
        out = tmp_path / "test.appack"
        project = _make_project()
        AppackCompiler(project).compile(out)
        prefix = str(project.project_id)
        with zipfile.ZipFile(out) as zf:
            names = set(zf.namelist())
        assert f"{prefix}/manifest.json" in names
        assert f"{prefix}/project.yaml" in names
        assert f"{prefix}/firmware_config.h" in names
        assert f"{prefix}/install_notes.txt" in names

    def test_manifest_fields(self, tmp_path: Path) -> None:
        out = tmp_path / "test.appack"
        project = _make_project()
        AppackCompiler(project).compile(out)
        prefix = str(project.project_id)
        with zipfile.ZipFile(out) as zf:
            manifest = json.loads(zf.read(f"{prefix}/manifest.json"))
        assert manifest["project_id"] == str(project.project_id)
        assert manifest["client_name"] == "Test Client"
        assert manifest["vessel_name"] == "Test Vessel"
        assert "checksums" in manifest
        assert "project_yaml_sha256" in manifest["checksums"]
        assert "firmware_config_h_sha256" in manifest["checksums"]

    def test_firmware_header_contains_defines(self, tmp_path: Path) -> None:
        out = tmp_path / "test.appack"
        project = _make_project()
        AppackCompiler(project).compile(out)
        prefix = str(project.project_id)
        with zipfile.ZipFile(out) as zf:
            header = zf.read(f"{prefix}/firmware_config.h").decode()
        assert "#define AR_RUDDER_ANGLE_LIMIT_DEG" in header
        assert "#define AR_PID_INNER_KP" in header
        assert "#define AR_PID_OUTER_KP" in header
        assert "#define AR_SENSOR_PRIMARY_TYPE" in header
        assert "ACTUATOR_HYDRAULIC_REVERSIBLE" in header
        assert "RUDDER_SENSOR_AS5048A_SPI" in header
        assert "35.0f" in header

    def test_firmware_header_dual_sensor(self, tmp_path: Path) -> None:
        out = tmp_path / "test.appack"
        project = _make_project(dual_sensors=True)
        AppackCompiler(project).compile(out)
        prefix = str(project.project_id)
        with zipfile.ZipFile(out) as zf:
            header = zf.read(f"{prefix}/firmware_config.h").decode()
            notes = zf.read(f"{prefix}/install_notes.txt").decode()
        assert "#define AR_SENSOR_REDUNDANT        1" in header
        assert "GPIO CS=11" in notes

    def test_firmware_header_single_sensor(self, tmp_path: Path) -> None:
        out = tmp_path / "test.appack"
        project = _make_project(dual_sensors=False)
        AppackCompiler(project).compile(out)
        prefix = str(project.project_id)
        with zipfile.ZipFile(out) as zf:
            header = zf.read(f"{prefix}/firmware_config.h").decode()
        assert "#define AR_SENSOR_REDUNDANT        0" in header

    def test_project_yaml_roundtrips(self, tmp_path: Path) -> None:
        out = tmp_path / "test.appack"
        project = _make_project()
        AppackCompiler(project).compile(out)
        prefix = str(project.project_id)
        with zipfile.ZipFile(out) as zf:
            yaml_text = zf.read(f"{prefix}/project.yaml").decode()
        restored = ProjectConfig.from_yaml(yaml_text)
        assert restored.client_name == project.client_name
        assert restored.vessel.name == project.vessel.name

    def test_empty_client_name_raises(self) -> None:
        project = _make_project()
        object.__setattr__(project, "client_name", "   ")
        with pytest.raises(ValueError, match="client_name"):
            AppackCompiler(project)

    def test_output_parent_created(self, tmp_path: Path) -> None:
        out = tmp_path / "deep" / "nested" / "output.appack"
        AppackCompiler(_make_project()).compile(out)
        assert out.exists()
