# Copyright (c) 2023-2024 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
"""test anta.models.py."""

# Mypy does not understand AntaTest.Input typing
# mypy: disable-error-code=attr-defined
from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING, Any, ClassVar

import pytest

from anta.decorators import deprecated_test, skip_on_platforms
from anta.models import AntaCommand, AntaTemplate, AntaTest
from tests.lib.fixture import DEVICE_HW_MODEL
from tests.lib.utils import generate_test_ids

if TYPE_CHECKING:
    from anta.device import AntaDevice


class FakeTest(AntaTest):
    """ANTA test that always succeed."""

    name = "FakeTest"
    description = "ANTA test that always succeed"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success()


class FakeTestWithFailedCommand(AntaTest):
    """ANTA test with a command that failed."""

    name = "FakeTestWithFailedCommand"
    description = "ANTA test with a command that failed"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version", errors=["failed command"])]

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success()


class FakeTestWithUnsupportedCommand(AntaTest):
    """ANTA test with an unsupported command."""

    name = "FakeTestWithUnsupportedCommand"
    description = "ANTA test with an unsupported command"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
        AntaCommand(
            command="show hardware counter drop",
            errors=["Unavailable command (not supported on this hardware platform) (at token 2: 'counter')"],
        )
    ]

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success()


class FakeTestWithInput(AntaTest):
    """ANTA test with inputs that always succeed."""

    name = "FakeTestWithInput"
    description = "ANTA test with inputs that always succeed"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

    class Input(AntaTest.Input):
        """Inputs for FakeTestWithInput test."""

        string: str

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success(self.inputs.string)


class FakeTestWithTemplate(AntaTest):
    """ANTA test with template that always succeed."""

    name = "FakeTestWithTemplate"
    description = "ANTA test with template that always succeed"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]

    class Input(AntaTest.Input):
        """Inputs for FakeTestWithTemplate test."""

        interface: str

    def render(self, template: AntaTemplate) -> list[AntaCommand]:
        """Render function."""
        return [template.render(interface=self.inputs.interface)]

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success(self.instance_commands[0].command)


class FakeTestWithTemplateNoRender(AntaTest):
    """ANTA test with template that miss the render() method."""

    name = "FakeTestWithTemplateNoRender"
    description = "ANTA test with template that miss the render() method"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]

    class Input(AntaTest.Input):
        """Inputs for FakeTestWithTemplateNoRender test."""

        interface: str

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success(self.instance_commands[0].command)


class FakeTestWithTemplateBadRender1(AntaTest):
    """ANTA test with template that raises a AntaTemplateRenderError exception."""

    name = "FakeTestWithTemplateBadRender"
    description = "ANTA test with template that raises a AntaTemplateRenderError exception"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]

    class Input(AntaTest.Input):
        """Inputs for FakeTestWithTemplateBadRender1 test."""

        interface: str

    def render(self, template: AntaTemplate) -> list[AntaCommand]:
        """Render function."""
        return [template.render(wrong_template_param=self.inputs.interface)]

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success(self.instance_commands[0].command)


class FakeTestWithTemplateBadRender2(AntaTest):
    """ANTA test with template that raises an arbitrary exception in render()."""

    name = "FakeTestWithTemplateBadRender2"
    description = "ANTA test with template that raises an arbitrary exception in render()"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]

    class Input(AntaTest.Input):
        """Inputs for FakeTestWithTemplateBadRender2 test."""

        interface: str

    def render(self, template: AntaTemplate) -> list[AntaCommand]:
        """Render function."""
        raise RuntimeError(template)

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success(self.instance_commands[0].command)


class FakeTestWithTemplateBadRender3(AntaTest):
    """ANTA test with template that gives extra template parameters in render()."""

    name = "FakeTestWithTemplateBadRender3"
    description = "ANTA test with template that gives extra template parameters in render()"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]

    class Input(AntaTest.Input):
        """Inputs for FakeTestWithTemplateBadRender3 test."""

        interface: str

    def render(self, template: AntaTemplate) -> list[AntaCommand]:
        """Render function."""
        return [template.render(interface=self.inputs.interface, extra="blah")]

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success(self.instance_commands[0].command)


class FakeTestWithTemplateBadTest(AntaTest):
    """ANTA test with template that tries to access an undefined template parameter in test()."""

    name = "FakeTestWithTemplateBadTest"
    description = "ANTA test with template that tries to access an undefined template parameter in test()"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]

    class Input(AntaTest.Input):
        """Inputs for FakeTestWithTemplateBadTest test."""

        interface: str

    def render(self, template: AntaTemplate) -> list[AntaCommand]:
        """Render function."""
        return [template.render(interface=self.inputs.interface)]

    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        # The following line must raise AttributeError at runtime
        self.result.is_success(self.instance_commands[0].params.wrong_template_param)


class SkipOnPlatformTest(AntaTest):
    """ANTA test that is skipped."""

    name = "SkipOnPlatformTest"
    description = "ANTA test that is skipped on a specific platform"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

    @skip_on_platforms([DEVICE_HW_MODEL])
    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success()


class UnSkipOnPlatformTest(AntaTest):
    """ANTA test that is skipped."""

    name = "UnSkipOnPlatformTest"
    description = "ANTA test that is skipped on a specific platform"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

    @skip_on_platforms(["dummy"])
    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success()


class SkipOnPlatformTestWithInput(AntaTest):
    """ANTA test skipped on platforms but with Input."""

    name = "SkipOnPlatformTestWithInput"
    description = "ANTA test skipped on platforms but with Input"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

    class Input(AntaTest.Input):
        """Inputs for SkipOnPlatformTestWithInput test."""

        string: str

    @skip_on_platforms([DEVICE_HW_MODEL])
    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success(self.inputs.string)


class DeprecatedTestWithoutNewTest(AntaTest):
    """ANTA test that is deprecated without new test."""

    name = "DeprecatedTestWitouthNewTest"
    description = "ANTA test that is deprecated without new test"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

    @deprecated_test()
    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success()


class DeprecatedTestWithNewTest(AntaTest):
    """ANTA test that is deprecated with new test."""

    name = "DeprecatedTestWithNewTest"
    description = "ANTA deprecated test with New Test"
    categories: ClassVar[list[str]] = []
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

    @deprecated_test(new_tests=["NewTest"])
    @AntaTest.anta_test
    def test(self) -> None:
        """Test function."""
        self.result.is_success()


ANTATEST_DATA: list[dict[str, Any]] = [
    {
        "name": "no input",
        "test": FakeTest,
        "inputs": None,
        "expected": {"__init__": {"result": "unset"}, "test": {"result": "success"}},
    },
    {
        "name": "extra input",
        "test": FakeTest,
        "inputs": {"string": "culpa! veniam quas quas veniam molestias, esse"},
        "expected": {
            "__init__": {
                "result": "error",
                "messages": ["Extra inputs are not permitted"],
            },
            "test": {"result": "error"},
        },
    },
    {
        "name": "no input",
        "test": FakeTestWithInput,
        "inputs": None,
        "expected": {
            "__init__": {"result": "error", "messages": ["Field required"]},
            "test": {"result": "error"},
        },
    },
    {
        "name": "wrong input type",
        "test": FakeTestWithInput,
        "inputs": {"string": 1},
        "expected": {
            "__init__": {
                "result": "error",
                "messages": ["Input should be a valid string"],
            },
            "test": {"result": "error"},
        },
    },
    {
        "name": "good input",
        "test": FakeTestWithInput,
        "inputs": {"string": "culpa! veniam quas quas veniam molestias, esse"},
        "expected": {
            "__init__": {"result": "unset"},
            "test": {
                "result": "success",
                "messages": ["culpa! veniam quas quas veniam molestias, esse"],
            },
        },
    },
    {
        "name": "good input",
        "test": FakeTestWithTemplate,
        "inputs": {"interface": "Ethernet1"},
        "expected": {
            "__init__": {"result": "unset"},
            "test": {"result": "success", "messages": ["show interface Ethernet1"]},
        },
    },
    {
        "name": "wrong input type",
        "test": FakeTestWithTemplate,
        "inputs": {"interface": 1},
        "expected": {
            "__init__": {
                "result": "error",
                "messages": ["Input should be a valid string"],
            },
            "test": {"result": "error"},
        },
    },
    {
        "name": "wrong render definition",
        "test": FakeTestWithTemplateNoRender,
        "inputs": {"interface": "Ethernet1"},
        "expected": {
            "__init__": {
                "result": "error",
                "messages": ["AntaTemplate are provided but render() method has not been implemented for tests.units.test_models.FakeTestWithTemplateNoRender"],
            },
            "test": {"result": "error"},
        },
    },
    {
        "name": "AntaTemplateRenderError",
        "test": FakeTestWithTemplateBadRender1,
        "inputs": {"interface": "Ethernet1"},
        "expected": {
            "__init__": {
                "result": "error",
                "messages": ["Cannot render template {template='show interface {interface}' version='latest' revision=None ofmt='json' use_cache=True}"],
            },
            "test": {"result": "error"},
        },
    },
    {
        "name": "RuntimeError in render()",
        "test": FakeTestWithTemplateBadRender2,
        "inputs": {"interface": "Ethernet1"},
        "expected": {
            "__init__": {
                "result": "error",
                "messages": ["Exception in tests.units.test_models.FakeTestWithTemplateBadRender2.render(): RuntimeError"],
            },
            "test": {"result": "error"},
        },
    },
    {
        "name": "Extra template parameters in render()",
        "test": FakeTestWithTemplateBadRender3,
        "inputs": {"interface": "Ethernet1"},
        "expected": {
            "__init__": {
                "result": "error",
                "messages": [
                    "Exception in tests.units.test_models.FakeTestWithTemplateBadRender3.render(): ValidationError: 1 validation error for AntaParams\n"
                    "extra\n"
                    "  Extra inputs are not permitted [type=extra_forbidden, input_value='blah', input_type=str]\n"
                ],
            },
            "test": {"result": "error"},
        },
    },
    {
        "name": "Access undefined template param in test()",
        "test": FakeTestWithTemplateBadTest,
        "inputs": {"interface": "Ethernet1"},
        "expected": {
            "__init__": {"result": "unset"},
            "test": {"result": "error", "messages": ["AttributeError: 'AntaParams' object has no attribute 'wrong_template_param'"]},
        },
    },
    {
        "name": "unskip on platforms",
        "test": UnSkipOnPlatformTest,
        "inputs": None,
        "expected": {
            "__init__": {"result": "unset"},
            "test": {"result": "success"},
        },
    },
    {
        "name": "skip on platforms, unset",
        "test": SkipOnPlatformTest,
        "inputs": None,
        "expected": {
            "__init__": {"result": "unset"},
            "test": {"result": "skipped"},
        },
    },
    {
        "name": "skip on platforms, not unset",
        "test": SkipOnPlatformTestWithInput,
        "inputs": None,
        "expected": {
            "__init__": {"result": "error", "messages": ["Field required"]},
            "test": {"result": "error"},
        },
    },
    {
        "name": "deprecate test without new test",
        "test": DeprecatedTestWithoutNewTest,
        "inputs": None,
        "expected": {
            "__init__": {"result": "unset"},
            "test": {"result": "success"},
        },
    },
    {
        "name": "deprecate test with new test",
        "test": DeprecatedTestWithNewTest,
        "inputs": None,
        "expected": {
            "__init__": {"result": "unset"},
            "test": {"result": "success"},
        },
    },
    {
        "name": "failed command",
        "test": FakeTestWithFailedCommand,
        "inputs": None,
        "expected": {
            "__init__": {"result": "unset"},
            "test": {
                "result": "error",
                "messages": ["show version has failed: failed command"],
            },
        },
    },
    {
        "name": "unsupported command",
        "test": FakeTestWithUnsupportedCommand,
        "inputs": None,
        "expected": {
            "__init__": {"result": "unset"},
            "test": {
                "result": "skipped",
                "messages": ["'show hardware counter drop' is not supported on pytest"],
            },
        },
    },
]


class TestAntaTest:
    """Test for anta.models.AntaTest."""

    def test__init_subclass__name(self) -> None:
        """Test __init_subclass__."""
        # Pylint detects all the classes in here as unused which is on purpose
        # pylint: disable=unused-variable
        with pytest.raises(NotImplementedError) as exec_info:

            class WrongTestNoName(AntaTest):
                """ANTA test that is missing a name."""

                description = "ANTA test that is missing a name"
                categories: ClassVar[list[str]] = []
                commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

                @AntaTest.anta_test
                def test(self) -> None:
                    self.result.is_success()

        assert exec_info.value.args[0] == "Class tests.units.test_models.WrongTestNoName is missing required class attribute name"

        with pytest.raises(NotImplementedError) as exec_info:

            class WrongTestNoDescription(AntaTest):
                """ANTA test that is missing a description."""

                name = "WrongTestNoDescription"
                categories: ClassVar[list[str]] = []
                commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

                @AntaTest.anta_test
                def test(self) -> None:
                    self.result.is_success()

        assert exec_info.value.args[0] == "Class tests.units.test_models.WrongTestNoDescription is missing required class attribute description"

        with pytest.raises(NotImplementedError) as exec_info:

            class WrongTestNoCategories(AntaTest):
                """ANTA test that is missing categories."""

                name = "WrongTestNoCategories"
                description = "ANTA test that is missing categories"
                commands: ClassVar[list[AntaCommand | AntaTemplate]] = []

                @AntaTest.anta_test
                def test(self) -> None:
                    self.result.is_success()

        assert exec_info.value.args[0] == "Class tests.units.test_models.WrongTestNoCategories is missing required class attribute categories"

        with pytest.raises(NotImplementedError) as exec_info:

            class WrongTestNoCommands(AntaTest):
                """ANTA test that is missing commands."""

                name = "WrongTestNoCommands"
                description = "ANTA test that is missing commands"
                categories: ClassVar[list[str]] = []

                @AntaTest.anta_test
                def test(self) -> None:
                    self.result.is_success()

        assert exec_info.value.args[0] == "Class tests.units.test_models.WrongTestNoCommands is missing required class attribute commands"

    def _assert_test(self, test: AntaTest, expected: dict[str, Any]) -> None:
        assert test.result.result == expected["result"]
        if "messages" in expected:
            for result_msg, expected_msg in zip(test.result.messages, expected["messages"]):  # NOTE: zip(strict=True) has been added in Python 3.10
                assert expected_msg in result_msg

    @pytest.mark.parametrize("data", ANTATEST_DATA, ids=generate_test_ids(ANTATEST_DATA))
    def test__init__(self, device: AntaDevice, data: dict[str, Any]) -> None:
        """Test the AntaTest constructor."""
        expected = data["expected"]["__init__"]
        test = data["test"](device, inputs=data["inputs"])
        self._assert_test(test, expected)

    @pytest.mark.parametrize("data", ANTATEST_DATA, ids=generate_test_ids(ANTATEST_DATA))
    def test_test(self, device: AntaDevice, data: dict[str, Any]) -> None:
        """Test the AntaTest.test method."""
        expected = data["expected"]["test"]
        test = data["test"](device, inputs=data["inputs"])
        asyncio.run(test.test())
        self._assert_test(test, expected)


ANTATEST_BLACKLIST_DATA = ["reload", "reload --force", "write", "wr mem"]


@pytest.mark.parametrize("data", ANTATEST_BLACKLIST_DATA)
def test_blacklist(device: AntaDevice, data: str) -> None:
    """Test for blacklisting function."""

    class FakeTestWithBlacklist(AntaTest):
        """Fake Test for blacklist."""

        name = "FakeTestWithBlacklist"
        description = "ANTA test that has blacklisted command"
        categories: ClassVar[list[str]] = []
        commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=data)]

        @AntaTest.anta_test
        def test(self) -> None:
            self.result.is_success()

    test_instance = FakeTestWithBlacklist(device)

    # Run the test() method
    asyncio.run(test_instance.test())
    assert test_instance.result.result == "error"


class TestAntaComamnd:
    """Test for anta.models.AntaCommand."""

    # ruff: noqa: B018
    # pylint: disable=pointless-statement

    def test_empty_output_access(self) -> None:
        """Test for both json and text ofmt."""
        json_cmd = AntaCommand(command="show dummy")
        text_cmd = AntaCommand(command="show dummy", ofmt="text")
        msg = "There is no output for command 'show dummy'"
        with pytest.raises(RuntimeError, match=msg):
            json_cmd.json_output
        with pytest.raises(RuntimeError, match=msg):
            text_cmd.text_output

    def test_wrong_format_output_access(self) -> None:
        """Test for both json and text ofmt."""
        json_cmd = AntaCommand(command="show dummy", output={})
        json_cmd_2 = AntaCommand(command="show dummy", output="not_json")
        text_cmd = AntaCommand(command="show dummy", ofmt="text", output="blah")
        text_cmd_2 = AntaCommand(command="show dummy", ofmt="text", output={"not_a": "string"})
        msg = "Output of command 'show dummy' is invalid"
        with pytest.raises(RuntimeError, match=msg):
            json_cmd.text_output
        with pytest.raises(RuntimeError, match=msg):
            text_cmd.json_output
        with pytest.raises(RuntimeError, match=msg):
            json_cmd_2.text_output
        with pytest.raises(RuntimeError, match=msg):
            text_cmd_2.json_output

    def test_supported(self) -> None:
        """Test if the supported property."""
        command = AntaCommand(command="show hardware counter drop", errors=["Unavailable command (not supported on this hardware platform) (at token 2: 'counter')"])
        assert command.supported is False
        command = AntaCommand(
            command="show hardware counter drop", output={"totalAdverseDrops": 0, "totalCongestionDrops": 0, "totalPacketProcessorDrops": 0, "dropEvents": {}}
        )
        assert command.supported is True

    def test_requires_privileges(self) -> None:
        """Test if the requires_privileges property."""
        command = AntaCommand(command="show aaa methods accounting", errors=["Invalid input (privileged mode required)"])
        assert command.requires_privileges is True
        command = AntaCommand(
            command="show aaa methods accounting",
            output={
                "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleMethods": []}},
                "execAcctMethods": {"exec": {"defaultMethods": [], "consoleMethods": []}},
                "systemAcctMethods": {"system": {"defaultMethods": [], "consoleMethods": []}},
                "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
            },
        )
        assert command.requires_privileges is False
