# Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ruff: noqa: E731

import json
import threading

import pytest
from httpx_sse import ServerSentEvent

from ansys.simai.core.data.optimizations import (
    _validate_axial_symmetry,
    _validate_bounding_boxes,
    _validate_global_coefficients_for_non_parametric,
    _validate_max_displacement,
    _validate_n_iters,
    _validate_outcome_constraints,
)
from ansys.simai.core.errors import InvalidArguments


def test_validate_bounding_boxes_success():
    bounding_boxes = [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]]

    _validate_bounding_boxes(bounding_boxes)


@pytest.mark.parametrize(
    "bounding_boxes, error_message",
    [
        ([], "bounding_boxes must be a non-empty list"),
        ([[0.1, 0.1, 0.1, 0.1]], "Bounding box at index 0 should be a list of 6 values"),
        (
            [[0.1, "foo", 0.1, 0.2, 0.1, 0.2]],
            "Bounding box at index 0 contains non-numeric values.",
        ),
        (
            [[0.1, 0.1, 0.1, 0.2, 0.1, 0.2]],
            r"Bounding box at index 0: xmin \(0\.1\) must be less than xmax \(0\.1\)",
        ),
        (
            [[0.1, 0.2, 0.1, 0.1, 0.1, 0.2]],
            r"Bounding box at index 0: ymin \(0\.1\) must be less than ymax \(0\.1\)",
        ),
        (
            [[0.1, 0.2, 0.1, 0.2, 0.1, 0.1]],
            r"Bounding box at index 0: zmin \(0\.1\) must be less than zmax \(0\.1\)",
        ),
    ],
)
def test_validate_bounding_boxes_fails(bounding_boxes, error_message):
    with pytest.raises(
        expected_exception=InvalidArguments,
        match=error_message,
    ):
        _validate_bounding_boxes(bounding_boxes)


def test_validate_global_coefficients_for_non_parametric_success():
    # Test with minimize only
    _validate_global_coefficients_for_non_parametric(minimize=["TotalForceX"], maximize=[])

    # Test with maximize only
    _validate_global_coefficients_for_non_parametric(minimize=[], maximize=["TotalForceY"])


@pytest.mark.parametrize(
    "minimize, maximize, error_message",
    [
        (
            ["TotalForceX"],
            ["TotalForceY"],
            "Only one of minimize or maximize can be provided for non parametric optimization.",
        ),
        (
            [],
            [],
            "minimize or maximize must be a list of one string for non parametric optimization.",
        ),
        (
            ["TotalForceX", "Pressure"],
            [],
            "minimize or maximize must be a list of one string for non parametric optimization.",
        ),
        (
            None,
            None,
            "minimize or maximize must be a list.",
        ),
        (
            {},
            [],
            "minimize or maximize must be a list of one string for non parametric optimization.",
        ),
        (
            [],
            None,
            "minimize or maximize must be a list of one string for non parametric optimization.",
        ),
        (
            "string_instead_of_list",
            [],
            "minimize or maximize must be a list of one string for non parametric optimization.",
        ),
    ],
)
def test_validate_global_coefficients_for_non_parametric_fails(minimize, maximize, error_message):
    with pytest.raises(
        expected_exception=InvalidArguments,
        match=error_message,
    ):
        _validate_global_coefficients_for_non_parametric(minimize, maximize)


def test_validate_outcome_constraints_success():
    outcome_constraints = ["TotalForceX <= 10", "Pressure >= 5.5"]

    _validate_outcome_constraints(outcome_constraints)


@pytest.mark.parametrize(
    "outcome_constraints, error_message",
    [
        (None, "outcome_constraints must be a list"),
        ([123], "Constraint at index 0 must be a string"),
        (
            ["TotalForceX > 10"],
            r"Constraint at index 0 \(TotalForceX > 10\) must contain either >= or <= operator",
        ),
        (
            ["TotalForceX <= 10 <= 20"],
            r"Constraint at index 0 \(TotalForceX <= 10 <= 20\) must be of form 'metric_name <= value'",
        ),
        (["<= 10"], r"Constraint at index 0 \(<= 10\) must have a metric name"),
        (
            ["TotalForceX <= abc"],
            r"Constraint at index 0 \(TotalForceX <= abc\): 'abc' must be a numeric value",
        ),
    ],
)
def test_validate_outcome_constraints_fails(outcome_constraints, error_message):
    with pytest.raises(
        expected_exception=InvalidArguments,
        match=error_message,
    ):
        _validate_outcome_constraints(outcome_constraints)


def test_validate_n_iters_success():
    n_iters = 10

    _validate_n_iters(n_iters)


@pytest.mark.parametrize(
    "n_iters, error_message",
    [
        (None, "n_iters must be an integer"),
        ("5", "n_iters must be an integer"),
        (0, "n_iters must be strictly positive"),
        (-5, "n_iters must be strictly positive"),
    ],
)
def test_validate_n_iters_fails(n_iters, error_message):
    with pytest.raises(
        expected_exception=InvalidArguments,
        match=error_message,
    ):
        _validate_n_iters(n_iters)


def validate_max_displacement_success():
    max_displacement = [1, 0.25]
    bounding_boxes = [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]]

    _validate_max_displacement(max_displacement, bounding_boxes)

    _validate_max_displacement(None, bounding_boxes)


@pytest.mark.parametrize(
    "max_displacement, bounding_boxes, error_message",
    [
        (
            "not_a_list",
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]],
            "max_displacement must be a list",
        ),
        (
            123,
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]],
            "max_displacement must be a list",
        ),
        (
            {},
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]],
            "max_displacement must be a list",
        ),
        (
            ["this_is_a_string"],
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]],
            "max_displacement contains non-numeric values",
        ),
        (
            [1, "perhaps_a_string"],
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]],
            "max_displacement contains non-numeric values",
        ),
        (
            [None],
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]],
            "max_displacement contains non-numeric values",
        ),
        (
            [1.0],
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]],
            "Max displacement list and bounding boxes list must have the same number of items",
        ),
        (
            [1.0, 2.0, 3.0],
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]],
            "Max displacement list and bounding boxes list must have the same number of items",
        ),
        (
            [],
            [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]],
            "Max displacement list and bounding boxes list must have the same number of items",
        ),
    ],
)
def test_validate_max_displacement_fails(max_displacement, bounding_boxes, error_message):
    with pytest.raises(
        expected_exception=InvalidArguments,
        match=error_message,
    ):
        _validate_max_displacement(max_displacement, bounding_boxes)


def validate_axial_symmetry_success():
    _validate_axial_symmetry("x", None)
    _validate_axial_symmetry(None, None)


@pytest.mark.parametrize(
    "axial_symmetry, symmetries",
    [
        ("a", None),
        (5, None),
        ("not_axis", None),
        (-1, None),
        ("x", ["x", "y"]),
    ],
)
def test_validate_axial_symmetry_fails(axial_symmetry, symmetries):
    with pytest.raises(
        expected_exception=InvalidArguments,
    ):
        _validate_axial_symmetry(axial_symmetry, symmetries)


def test_run_non_parametric_optimization(simai_client, geometry_factory, httpx_mock):
    workspace_id = "insert_cool_reference"
    geometry = geometry_factory(workspace_id=workspace_id)

    httpx_mock.add_response(
        method="POST",
        url=f"https://test.test/workspaces/{workspace_id}/optimizations",
        status_code=202,
        json={"id": "wow", "state": "requested", "trial_runs": []},
    )
    httpx_mock.add_response(
        method="POST",
        url="https://test.test/optimizations/wow/trial-runs",
        status_code=202,
        json={"id": "wow1", "state": "requested"},
    )
    httpx_mock.add_response(
        method="POST",
        url="https://test.test/optimizations/wow/trial-runs",
        status_code=202,
        json={"id": "wow2", "state": "requested"},
    )
    httpx_mock.add_response(
        method="POST",
        url="https://test.test/optimizations/wow/trial-runs",
        status_code=202,
        json={"id": "wow3", "state": "requested"},
    )
    threading.Timer(
        0.1,
        simai_client._api._handle_sse_event,
        args=[
            ServerSentEvent(
                data=json.dumps(
                    {
                        "type": "job",
                        "status": "successful",
                        "record": {
                            "id": "wow",
                            "state": "successful",
                            "initial_geometry_parameters": None,
                        },
                        "target": {"type": "optimization", "id": "wow"},
                    }
                )
            )
        ],
    ).start()
    for i in range(1, 4):
        threading.Timer(
            i / 10 + 0.1,
            simai_client._api._handle_sse_event,
            args=[
                ServerSentEvent(
                    data=json.dumps(
                        {
                            "type": "job",
                            "status": "successful",
                            "record": {
                                "id": f"wow{i}",
                                "state": "successful",
                                "is_feasible": "true",
                                "outcome_values": i,
                                "next_geometry_parameters": None,
                                "geometry": {
                                    "id": f"x{i}",
                                    "name": f"x{i}",
                                    "workspace_id": geometry._fields["workspace_id"],
                                },
                            },
                            "target": {"type": "optimization_trial_run", "id": f"wow{i}"},
                        }
                    )
                )
            ],
        ).start()
    results = simai_client.optimizations.run_non_parametric(
        geometry=geometry,
        bounding_boxes=[[0.1, 1, 0.1, 1, 0.1, 1]],
        symmetries=["x", "y", "z"],
        minimize=["TotalForceX"],
        scalars={"VelocityX": 10.5},
        n_iters=3,
    )
    assert len(results.list_objectives()) == 3
    assert results.list_objectives() == [1, 2, 3]
    assert len(results.list_geometries()) == 3
    assert results.list_geometries()[0].name == "x1"
    assert results.optimization.id == "wow"
