"""Integration tests for routeros.introspect — requires a live RouterOS device."""

from __future__ import annotations

import pytest

import routeros
from routeros.introspect import discover_api, load_api_schema


@pytest.mark.integration
class TestDiscoverApiIntegration:
    def test_discover_returns_schema(self, client, tmp_path):
        """discover_api returns a well-formed schema from a live router."""
        schema = discover_api(client, data_dir=tmp_path)

        assert "version" in schema
        assert "generated_at" in schema
        assert "endpoint_count" in schema
        assert "tree" in schema
        assert schema["endpoint_count"] > 0

    def test_version_matches_system_resource(self, client, tmp_path):
        """Discovered version should match the normalised /system/resource/print version."""
        reply = client.run("/system/resource/print")
        raw_version = reply.re[0].map["version"]
        expected_version = raw_version.split()[0]

        schema = discover_api(client, data_dir=tmp_path)
        assert schema["version"] == expected_version

    def test_common_paths_present(self, client, tmp_path):
        """Standard RouterOS paths must appear at the top level of the tree."""
        schema = discover_api(client, data_dir=tmp_path)
        tree = schema["tree"]

        # These paths exist on every RouterOS 7.x device.
        for path in ("ip", "system", "interface"):
            assert path in tree, f"Expected path {path!r} in tree"
            assert tree[path]["_type"] in ("dir", "path"), (
                f"{path!r} should be _type=dir or path"
            )

    def test_tree_structure_is_valid(self, client, tmp_path):
        """Every node in the tree must have a valid _type."""
        schema = discover_api(client, data_dir=tmp_path)

        def check_node(node: dict, path: str) -> None:
            node_type = node.get("_type", "")
            assert node_type in ("dir", "path", "cmd", "arg"), (
                f"Invalid _type {node_type!r} at {path!r}"
            )
            if node_type in ("dir", "path"):
                for key, child in node.items():
                    if key == "_type":
                        continue
                    if isinstance(child, dict):
                        check_node(child, f"{path}/{key}")
            elif node_type == "cmd":
                for key, child in node.items():
                    if key == "_type":
                        continue
                    if isinstance(child, dict):
                        assert child.get("_type") == "arg", (
                            f"Expected _type=arg for {path}/{key}"
                        )

        for top_key, top_node in schema["tree"].items():
            if isinstance(top_node, dict):
                check_node(top_node, top_key)

    def test_cmd_nodes_have_arg_children(self, client, tmp_path):
        """Command nodes should contain only arg children (or be empty)."""
        schema = discover_api(client, data_dir=tmp_path)

        def find_cmd_with_args(tree: dict) -> tuple[str, dict] | None:
            for key, val in tree.items():
                if not isinstance(val, dict):
                    continue
                if val.get("_type") == "cmd":
                    args = {k: v for k, v in val.items() if k != "_type"}
                    if args:
                        return key, val
                elif val.get("_type") == "dir":
                    found = find_cmd_with_args(val)
                    if found:
                        return found
            return None

        found = find_cmd_with_args(schema["tree"])
        assert found is not None, "Expected to find at least one cmd with args"
        _, cmd_node = found
        for k, v in cmd_node.items():
            if k == "_type":
                continue
            assert isinstance(v, dict), f"Arg value for {k!r} must be a dict"
            assert v.get("_type") == "arg", f"Arg {k!r} must have _type=arg"

    def test_second_call_uses_cache(self, client, tmp_path):
        """Calling discover_api twice returns the same data from the cached file."""
        schema1 = discover_api(client, data_dir=tmp_path)
        schema2 = discover_api(client, data_dir=tmp_path)
        assert schema1 == schema2
        out = tmp_path / f"routeros_{schema1['version']}.json"
        assert out.exists()

    def test_force_regenerates_file(self, client, tmp_path):
        """force=True should overwrite the existing file."""
        schema1 = discover_api(client, data_dir=tmp_path)
        schema2 = discover_api(client, data_dir=tmp_path, force=True)

        assert schema2["version"] == schema1["version"]
        assert schema2["endpoint_count"] == schema1["endpoint_count"]

    def test_saved_file_loadable(self, client, tmp_path):
        """load_api_schema should return the file saved by discover_api."""
        schema = discover_api(client, data_dir=tmp_path)
        loaded = load_api_schema(schema["version"], data_dir=tmp_path)

        assert loaded is not None
        assert loaded["version"] == schema["version"]
        assert loaded["endpoint_count"] == schema["endpoint_count"]

    def test_public_api_accessible(self, client, tmp_path):
        """discover_api and load_api_schema must be accessible via routeros.*."""
        assert hasattr(routeros, "discover_api")
        assert hasattr(routeros, "load_api_schema")
        schema = routeros.discover_api(client, data_dir=tmp_path)
        assert schema["endpoint_count"] > 0
        assert "tree" in schema
