"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

import json
import uuid
from unittest.mock import AsyncMock

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from control_backend.api.v1.endpoints import program
from control_backend.schemas.program import BasicNorm, Goal, Phase, Plan, Program


@pytest.fixture
def app():
    """Create a FastAPI app with the /program route and mock socket."""
    app = FastAPI()
    app.include_router(program.router)
    return app


@pytest.fixture
def client(app):
    """Create a TestClient."""
    return TestClient(app)


def make_valid_program_dict():
    """Helper to create a valid Program JSON structure."""
    # Converting to JSON using Pydantic because it knows how to convert a UUID object
    program_json_str = Program(
        phases=[
            Phase(
                id=uuid.uuid4(),
                name="Basic Phase",
                norms=[
                    BasicNorm(
                        id=uuid.uuid4(),
                        name="Some norm",
                        norm="Do normal.",
                    ),
                ],
                goals=[
                    Goal(
                        id=uuid.uuid4(),
                        name="Some goal",
                        description="This description can be used to determine whether the goal "
                        "has been achieved.",
                        plan=Plan(
                            id=uuid.uuid4(),
                            name="Goal Plan",
                            steps=[],
                        ),
                        can_fail=False,
                    ),
                ],
                triggers=[],
            ),
        ],
    ).model_dump_json()
    # Converting back to a dict because that's what's expected
    return json.loads(program_json_str)


def test_receive_program_success(client):
    """Valid Program JSON should be parsed and sent through the socket."""
    mock_pub_socket = AsyncMock()
    client.app.state.endpoints_pub_socket = mock_pub_socket

    program_dict = make_valid_program_dict()

    response = client.post("/program", json=program_dict)

    assert response.status_code == 202
    assert response.json() == {"status": "Program parsed"}

    # Verify socket call
    mock_pub_socket.send_multipart.assert_awaited_once()
    args, kwargs = mock_pub_socket.send_multipart.await_args

    assert args[0][0] == b"program"

    sent_bytes = args[0][1]
    sent_obj = json.loads(sent_bytes.decode())

    # Converting to JSON using Pydantic because it knows how to handle UUIDs
    expected_obj = json.loads(Program.model_validate(program_dict).model_dump_json())
    assert sent_obj == expected_obj


def test_receive_program_invalid_json(client):
    """
    Invalid JSON (malformed) -> FastAPI never calls endpoint.
    It returns a 422 Unprocessable Entity.
    """
    mock_pub_socket = AsyncMock()
    client.app.state.endpoints_pub_socket = mock_pub_socket

    # FastAPI only accepts valid JSON bodies, so send raw string
    response = client.post("/program", content="{invalid json}")

    assert response.status_code == 422
    mock_pub_socket.send_multipart.assert_not_called()


def test_receive_program_invalid_deep_structure(client):
    """
    Valid JSON but schema invalid -> Pydantic throws validation error -> 422.
    """
    mock_pub_socket = AsyncMock()
    client.app.state.endpoints_pub_socket = mock_pub_socket

    # Missing "value" in norms element
    bad_program = {
        "phases": [
            {
                "id": "phase1",
                "name": "deepfail",
                "nextPhaseId": "phase2",
                "phaseData": {
                    "norms": [
                        {"id": "n1", "name": "norm"}  # INVALID: missing "value"
                    ],
                    "goals": [
                        {"id": "g1", "name": "goal", "description": "desc", "achieved": False}
                    ],
                    "triggers": [
                        {"id": "t1", "label": "trigger", "type": "keyword", "value": ["start"]}
                    ],
                },
            }
        ]
    }

    response = client.post("/program", json=bad_program)

    assert response.status_code == 422
    mock_pub_socket.send_multipart.assert_not_called()
