"""
Tests for the Velxio MCP server tools.

These tests exercise the tool functions directly (without a running MCP transport)
and mock the ArduinoCLIService to avoid requiring arduino-cli to be installed.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest

# Ensure the backend package is importable when running from the repo root
sys.path.insert(0, str(Path(__file__).parent.parent))


# ---------------------------------------------------------------------------
# Wokwi utility tests
# ---------------------------------------------------------------------------

from app.mcp.wokwi import (
    color_to_hex,
    format_wokwi_diagram,
    generate_arduino_sketch,
    hex_to_color_name,
    parse_wokwi_diagram,
)


class TestColorHelpers:
    def test_named_color_to_hex(self):
        assert color_to_hex("red") == "#ff0000"
        assert color_to_hex("GREEN") == "#00ff00"

    def test_hex_passthrough(self):
        assert color_to_hex("#1a2b3c") == "#1a2b3c"

    def test_hex_to_name(self):
        assert hex_to_color_name("#ff0000") == "red"
        assert hex_to_color_name("#unknown") == "#unknown"


SAMPLE_DIAGRAM = {
    "version": 1,
    "author": "test",
    "editor": "wokwi",
    "parts": [
        {"type": "wokwi-arduino-uno", "id": "uno", "top": 0, "left": 0, "attrs": {}},
        {
            "type": "wokwi-led",
            "id": "led1",
            "top": 100,
            "left": 200,
            "rotate": 0,
            "attrs": {"color": "red"},
        },
        {
            "type": "wokwi-resistor",
            "id": "r1",
            "top": 150,
            "left": 200,
            "attrs": {"value": "220"},
        },
    ],
    "connections": [
        ["uno:13", "led1:A", "green", []],
        ["led1:C", "r1:1", "black", []],
        ["r1:2", "uno:GND.1", "black", []],
    ],
}


class TestParseWokwiDiagram:
    def test_detects_board_fqbn(self):
        result = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        assert result["board_fqbn"] == "arduino:avr:uno"

    def test_components_parsed(self):
        result = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        ids = [c["id"] for c in result["components"]]
        assert "uno" in ids
        assert "led1" in ids
        assert "r1" in ids

    def test_connections_parsed(self):
        result = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        conns = result["connections"]
        assert len(conns) == 3
        first = conns[0]
        assert first["from_part"] == "uno"
        assert first["from_pin"] == "13"
        assert first["to_part"] == "led1"
        assert first["to_pin"] == "A"
        assert first["color"] == "#00ff00"  # 'green' -> hex

    def test_version(self):
        result = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        assert result["version"] == 1

    def test_empty_diagram(self):
        result = parse_wokwi_diagram({})
        assert result["board_fqbn"] == "arduino:avr:uno"
        assert result["components"] == []
        assert result["connections"] == []

    def test_unknown_board_defaults(self):
        diagram = {"parts": [{"type": "unknown-board", "id": "b1", "attrs": {}}]}
        result = parse_wokwi_diagram(diagram)
        assert result["board_fqbn"] == "arduino:avr:uno"


class TestFormatWokwiDiagram:
    def test_round_trip(self):
        """parse → format should preserve the essential structure."""
        circuit = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        wokwi = format_wokwi_diagram(circuit)

        part_ids = [p["id"] for p in wokwi["parts"]]
        assert "led1" in part_ids
        assert "r1" in part_ids

    def test_board_injected(self):
        circuit = {
            "board_fqbn": "arduino:avr:uno",
            "components": [],
            "connections": [],
            "version": 1,
        }
        wokwi = format_wokwi_diagram(circuit)
        types = [p["type"] for p in wokwi["parts"]]
        assert "wokwi-arduino-uno" in types

    def test_author_embedded(self):
        circuit = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        wokwi = format_wokwi_diagram(circuit, author="alice")
        assert wokwi["author"] == "alice"

    def test_connections_formatted(self):
        circuit = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        wokwi = format_wokwi_diagram(circuit)
        assert len(wokwi["connections"]) == 3
        # Each connection should be [from, to, color, []]
        for conn in wokwi["connections"]:
            assert isinstance(conn, list)
            assert len(conn) == 4


class TestGenerateArduinoSketch:
    def test_generates_setup_and_loop(self):
        circuit = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        sketch = generate_arduino_sketch(circuit)
        assert "void setup()" in sketch
        assert "void loop()" in sketch

    def test_contains_serial_begin(self):
        circuit = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        sketch = generate_arduino_sketch(circuit)
        assert "Serial.begin" in sketch


# ---------------------------------------------------------------------------
# MCP tool function tests
# ---------------------------------------------------------------------------

from app.mcp.server import (
    compile_project,
    create_circuit,
    export_wokwi_json,
    generate_code_files,
    import_wokwi_json,
    run_project,
    update_circuit,
)


class TestImportWokwiJson:
    @pytest.mark.asyncio
    async def test_valid_diagram(self):
        result = await import_wokwi_json(json.dumps(SAMPLE_DIAGRAM))
        assert "board_fqbn" in result
        assert "components" in result
        assert "connections" in result

    @pytest.mark.asyncio
    async def test_invalid_json(self):
        result = await import_wokwi_json("not-json{")
        assert "error" in result

    @pytest.mark.asyncio
    async def test_not_object(self):
        result = await import_wokwi_json("[1, 2, 3]")
        assert "error" in result


class TestExportWokwiJson:
    @pytest.mark.asyncio
    async def test_valid_circuit(self):
        circuit = parse_wokwi_diagram(SAMPLE_DIAGRAM)
        result = await export_wokwi_json(circuit)
        assert "parts" in result
        assert "connections" in result
        assert result["editor"] == "velxio"

    @pytest.mark.asyncio
    async def test_invalid_input(self):
        result = await export_wokwi_json("not a dict")  # type: ignore[arg-type]
        assert "error" in result


class TestCreateCircuit:
    @pytest.mark.asyncio
    async def test_defaults(self):
        result = await create_circuit()
        assert result["board_fqbn"] == "arduino:avr:uno"
        assert result["components"] == []
        assert result["connections"] == []

    @pytest.mark.asyncio
    async def test_with_components(self):
        result = await create_circuit(
            board_fqbn="arduino:avr:uno",
            components=[{"id": "led1", "type": "wokwi-led", "left": 100, "top": 50}],
            connections=[
                {
                    "from_part": "uno",
                    "from_pin": "13",
                    "to_part": "led1",
                    "to_pin": "A",
                    "color": "green",
                }
            ],
        )
        assert len(result["components"]) == 1
        assert result["components"][0]["id"] == "led1"
        assert len(result["connections"]) == 1


class TestUpdateCircuit:
    @pytest.mark.asyncio
    async def test_add_component(self):
        circuit = await create_circuit(
            components=[{"id": "led1", "type": "wokwi-led"}]
        )
        updated = await update_circuit(
            circuit=circuit,
            add_components=[{"id": "btn1", "type": "wokwi-pushbutton"}],
        )
        ids = [c["id"] for c in updated["components"]]
        assert "led1" in ids
        assert "btn1" in ids

    @pytest.mark.asyncio
    async def test_remove_component(self):
        circuit = await create_circuit(
            components=[
                {"id": "led1", "type": "wokwi-led"},
                {"id": "r1", "type": "wokwi-resistor"},
            ]
        )
        updated = await update_circuit(
            circuit=circuit,
            remove_component_ids=["r1"],
        )
        ids = [c["id"] for c in updated["components"]]
        assert "led1" in ids
        assert "r1" not in ids

    @pytest.mark.asyncio
    async def test_change_board(self):
        circuit = await create_circuit()
        updated = await update_circuit(
            circuit=circuit,
            board_fqbn="rp2040:rp2040:rpipico",
        )
        assert updated["board_fqbn"] == "rp2040:rp2040:rpipico"

    @pytest.mark.asyncio
    async def test_add_and_remove_connections(self):
        circuit = await create_circuit(
            connections=[
                {
                    "from_part": "uno",
                    "from_pin": "13",
                    "to_part": "led1",
                    "to_pin": "A",
                }
            ]
        )
        updated = await update_circuit(
            circuit=circuit,
            add_connections=[
                {
                    "from_part": "uno",
                    "from_pin": "12",
                    "to_part": "led2",
                    "to_pin": "A",
                }
            ],
            remove_connections=[
                {
                    "from_part": "uno",
                    "from_pin": "13",
                    "to_part": "led1",
                    "to_pin": "A",
                }
            ],
        )
        assert len(updated["connections"]) == 1
        assert updated["connections"][0]["from_pin"] == "12"

    @pytest.mark.asyncio
    async def test_invalid_circuit(self):
        result = await update_circuit(circuit="bad")  # type: ignore[arg-type]
        assert "error" in result


class TestGenerateCodeFiles:
    @pytest.mark.asyncio
    async def test_generates_ino_file(self):
        circuit = await create_circuit()
        result = await generate_code_files(circuit=circuit)
        assert len(result["files"]) == 1
        assert result["files"][0]["name"] == "sketch.ino"

    @pytest.mark.asyncio
    async def test_custom_sketch_name(self):
        circuit = await create_circuit()
        result = await generate_code_files(circuit=circuit, sketch_name="blink")
        assert result["files"][0]["name"] == "blink.ino"

    @pytest.mark.asyncio
    async def test_extra_instructions(self):
        circuit = await create_circuit()
        result = await generate_code_files(
            circuit=circuit,
            extra_instructions="Generated by agent",
        )
        assert "Generated by agent" in result["files"][0]["content"]

    @pytest.mark.asyncio
    async def test_invalid_circuit(self):
        result = await generate_code_files(circuit="bad")  # type: ignore[arg-type]
        assert "error" in result


class TestCompileProject:
    @pytest.mark.asyncio
    async def test_missing_keys(self):
        result = await compile_project(files=[{"name": "sketch.ino"}])
        assert result["success"] is False
        assert "content" in result["error"]

    @pytest.mark.asyncio
    async def test_calls_arduino_cli(self):
        mock_result = {
            "success": True,
            "hex_content": ":00000001FF",
            "binary_content": None,
            "binary_type": None,
            "stdout": "",
            "stderr": "",
        }
        with patch(
            "app.mcp.server._arduino.compile",
            new=AsyncMock(return_value=mock_result),
        ):
            result = await compile_project(
                files=[{"name": "sketch.ino", "content": "void setup(){} void loop(){}"}]
            )
        assert result["success"] is True
        assert result["hex_content"] is not None


class TestRunProject:
    @pytest.mark.asyncio
    async def test_simulation_ready_flag_on_success(self):
        mock_result = {
            "success": True,
            "hex_content": ":00000001FF",
            "binary_content": None,
            "binary_type": None,
            "stdout": "",
            "stderr": "",
        }
        with patch(
            "app.mcp.server._arduino.compile",
            new=AsyncMock(return_value=mock_result),
        ):
            result = await run_project(
                files=[{"name": "sketch.ino", "content": "void setup(){} void loop(){}"}]
            )
        assert result["simulation_ready"] is True

    @pytest.mark.asyncio
    async def test_simulation_ready_flag_on_failure(self):
        mock_result = {
            "success": False,
            "hex_content": None,
            "binary_content": None,
            "binary_type": None,
            "stdout": "",
            "stderr": "error",
            "error": "Compilation failed",
        }
        with patch(
            "app.mcp.server._arduino.compile",
            new=AsyncMock(return_value=mock_result),
        ):
            result = await run_project(
                files=[{"name": "sketch.ino", "content": "bad code{{{"}]
            )
        assert result["simulation_ready"] is False
