"""Tests for the router module."""

import json
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock

import pytest

from augur_mcp.router import (
    SERVICE_MAP,
    build_args,
    build_method_name,
    get_resource_map,
    load_catalog,
    parse_resource_path,
    resolve_and_call,
    singularize,
    to_snake,
)


# --- to_snake ---


class TestToSnake:
    def test_kebab_to_snake(self):
        assert to_snake("agr-site") == "agr_site"

    def test_multiple_hyphens(self):
        assert to_snake("geo-codes-postal-codes") == "geo_codes_postal_codes"

    def test_no_hyphens(self):
        assert to_snake("items") == "items"

    def test_single_char(self):
        assert to_snake("a") == "a"


# --- singularize ---


class TestSingularize:
    def test_regular_plural(self):
        assert singularize("conversations") == "conversation"

    def test_ies_plural(self):
        assert singularize("entries") == "entry"

    def test_ses_plural(self):
        assert singularize("addresses") == "address"

    def test_already_singular(self):
        assert singularize("item") == "item"

    def test_single_s(self):
        assert singularize("messages") == "message"


# --- parse_resource_path ---


class TestParseResourcePath:
    def test_simple_resource(self):
        root, ids, child = parse_resource_path("settings")
        assert root == "settings"
        assert ids == []
        assert child is None

    def test_kebab_resource(self):
        root, ids, child = parse_resource_path("inv-mast")
        assert root == "inv_mast"
        assert ids == []
        assert child is None

    def test_nested_one_level(self):
        root, ids, child = parse_resource_path("training/1/conversations")
        assert root == "training"
        assert ids == [1]
        assert child == "conversations"

    def test_nested_two_levels(self):
        root, ids, child = parse_resource_path("training/1/conversations/5/messages")
        assert root == "training"
        assert ids == [1, 5]
        assert child == "messages"

    def test_nested_kebab_child(self):
        root, ids, child = parse_resource_path("inv-mast/1/attribute-values")
        assert root == "inv_mast"
        assert ids == [1]
        assert child == "attribute_values"


# --- build_method_name ---


class TestBuildMethodName:
    def test_simple_list(self):
        assert build_method_name("list", None) == "list"

    def test_simple_get(self):
        assert build_method_name("get", None) == "get"

    def test_simple_create(self):
        assert build_method_name("create", None) == "create"

    def test_simple_update(self):
        assert build_method_name("update", None) == "update"

    def test_simple_delete(self):
        assert build_method_name("delete", None) == "delete"

    def test_nested_list(self):
        assert build_method_name("list", "conversations") == "list_conversations"

    def test_nested_get(self):
        assert build_method_name("get", "conversations") == "get_conversation"

    def test_nested_create(self):
        assert build_method_name("create", "conversations") == "create_conversation"

    def test_nested_update(self):
        assert build_method_name("update", "messages") == "update_message"

    def test_nested_delete(self):
        assert build_method_name("delete", "messages") == "delete_message"


# --- build_args ---


class TestBuildArgs:
    def test_list_no_params(self):
        args = build_args("list", [], None)
        assert args == []

    def test_list_with_params(self):
        params = {"limit": 10}
        args = build_args("list", [], None, params=params)
        assert args == [{"limit": 10}]

    def test_get_simple(self):
        args = build_args("get", [], None, record_id="123")
        assert args == [123]

    def test_get_string_id(self):
        args = build_args("get", [], None, record_id="abc")
        assert args == ["abc"]

    def test_create_simple(self):
        data = {"name": "test"}
        args = build_args("create", [], None, data=data)
        assert args == [{"name": "test"}]

    def test_update_simple(self):
        data = {"name": "updated"}
        args = build_args("update", [], None, record_id="5", data=data)
        assert args == [5, {"name": "updated"}]

    def test_delete_simple(self):
        args = build_args("delete", [], None, record_id="5")
        assert args == [5]

    def test_nested_list(self):
        args = build_args("list", [1], "conversations", params={"limit": 5})
        assert args == [1, {"limit": 5}]

    def test_nested_get(self):
        args = build_args("get", [1], "conversations", record_id="5")
        assert args == [1, 5]

    def test_nested_create(self):
        data = {"body": "hello"}
        args = build_args("create", [1], "conversations", data=data)
        assert args == [1, {"body": "hello"}]

    def test_nested_update(self):
        data = {"body": "updated"}
        args = build_args("update", [1], "conversations", record_id="5", data=data)
        assert args == [1, 5, {"body": "updated"}]

    def test_nested_delete(self):
        args = build_args("delete", [1], "conversations", record_id="5")
        assert args == [1, 5]

    def test_deeply_nested_list(self):
        args = build_args("list", [1, 5], "messages", params={"limit": 10})
        assert args == [1, 5, {"limit": 10}]

    def test_get_no_id(self):
        args = build_args("get", [], None)
        assert args == []

    def test_create_no_data(self):
        args = build_args("create", [], None)
        assert args == []

    def test_update_no_id_no_data(self):
        args = build_args("update", [], None)
        assert args == []

    def test_delete_no_id(self):
        args = build_args("delete", [], None)
        assert args == []

    def test_update_with_id_no_data(self):
        args = build_args("update", [], None, record_id="5")
        assert args == [5]

    def test_update_with_data_no_id(self):
        args = build_args("update", [], None, data={"name": "x"})
        assert args == [{"name": "x"}]

    def test_unknown_action(self):
        """Unknown action returns only parent_ids."""
        args = build_args("unknown", [1], "child")
        assert args == [1]


# --- load_catalog ---


class TestLoadCatalog:
    def test_load_bundled(self, endpoints_dir):
        catalog = load_catalog(endpoints_dir)
        assert len(catalog) > 0
        assert "agr-site" in catalog
        assert "agr-info" in catalog

    def test_load_empty_dir(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            catalog = load_catalog(Path(tmpdir))
            assert catalog == {}

    def test_load_nonexistent_dir(self):
        catalog = load_catalog(Path("/nonexistent"))
        assert catalog == {}

    def test_endpoint_structure(self, endpoints_dir):
        catalog = load_catalog(endpoints_dir)
        for ep in catalog["agr-site"]:
            assert "method" in ep
            assert "path" in ep
            assert ep["method"] in ("GET", "POST", "PUT", "DELETE")

    def test_load_custom_jsonl(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            jsonl_path = Path(tmpdir) / "test-svc.jsonl"
            jsonl_path.write_text(
                json.dumps({"method": "GET", "path": "/api/items", "pathParams": []}) + "\n"
            )
            catalog = load_catalog(Path(tmpdir))
            assert "test-svc" in catalog
            assert len(catalog["test-svc"]) == 1

    def test_load_default_path(self):
        catalog = load_catalog()
        assert len(catalog) > 0
        assert "agr-site" in catalog

    def test_load_skips_empty_lines(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            jsonl_path = Path(tmpdir) / "test-svc.jsonl"
            jsonl_path.write_text(
                json.dumps({"method": "GET", "path": "/api/items"}) + "\n\n\n"
            )
            catalog = load_catalog(Path(tmpdir))
            assert len(catalog["test-svc"]) == 1


# --- get_resource_map ---


class TestGetResourceMap:
    def test_simple_endpoints(self):
        endpoints = [
            {"method": "GET", "path": "/api/settings"},
            {"method": "POST", "path": "/api/settings"},
            {"method": "GET", "path": "/api/settings/{settingsUid}"},
            {"method": "PUT", "path": "/api/settings/{settingsUid}"},
            {"method": "DELETE", "path": "/api/settings/{settingsUid}"},
        ]
        resource_map = get_resource_map(endpoints)
        assert "settings" in resource_map
        assert sorted(resource_map["settings"]) == ["DELETE", "GET", "POST", "PUT"]

    def test_nested_endpoints(self):
        endpoints = [
            {"method": "GET", "path": "/api/training"},
            {"method": "GET", "path": "/api/training/{id}/conversations"},
            {"method": "POST", "path": "/api/training/{id}/conversations"},
        ]
        resource_map = get_resource_map(endpoints)
        assert "training" in resource_map
        assert "training/conversations" in resource_map

    def test_health_and_utility(self):
        endpoints = [
            {"method": "GET", "path": "/api/health-check"},
            {"method": "GET", "path": "/api/ping"},
            {"method": "GET", "path": "/api/whoami"},
        ]
        resource_map = get_resource_map(endpoints)
        assert "health-check" in resource_map
        assert "ping" in resource_map
        assert "whoami" in resource_map


# --- resolve_and_call ---


class TestResolveAndCall:
    def test_unknown_service(self, mock_api):
        with pytest.raises(ValueError, match="Unknown service"):
            resolve_and_call(mock_api, "nonexistent", "resource", "list")

    def test_unknown_resource(self, mock_api):
        mock_svc = MagicMock()
        mock_svc.configure_mock(**{"nonexistent": MagicMock(side_effect=AttributeError)})
        del mock_svc.nonexistent
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)
        with pytest.raises(ValueError, match="Unknown resource"):
            resolve_and_call(mock_api, "agr-site", "nonexistent", "list")

    def test_unknown_method(self, mock_api):
        mock_resource = MagicMock(spec=[])
        mock_svc = MagicMock()
        mock_svc.settings = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)
        with pytest.raises(ValueError, match="No method"):
            resolve_and_call(mock_api, "agr-site", "settings", "list")

    def test_list_simple(self, mock_api):
        mock_response = MagicMock()
        mock_response.model_dump.return_value = {"data": [], "count": 0, "total": 0}
        mock_resource = MagicMock()
        mock_resource.list.return_value = mock_response
        mock_svc = MagicMock()
        mock_svc.settings = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(mock_api, "agr-site", "settings", "list")
        assert result == {"data": [], "count": 0, "total": 0}
        mock_resource.list.assert_called_once_with()

    def test_list_with_params(self, mock_api):
        mock_response = MagicMock()
        mock_response.model_dump.return_value = {"data": [{"id": 1}], "count": 1, "total": 1}
        mock_resource = MagicMock()
        mock_resource.list.return_value = mock_response
        mock_svc = MagicMock()
        mock_svc.settings = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(
            mock_api, "agr-site", "settings", "list", params={"limit": 10}
        )
        assert result["count"] == 1
        mock_resource.list.assert_called_once_with({"limit": 10})

    def test_get_simple(self, mock_api):
        mock_response = MagicMock()
        mock_response.model_dump.return_value = {"data": {"id": 5}, "count": 1}
        mock_resource = MagicMock()
        mock_resource.get.return_value = mock_response
        mock_svc = MagicMock()
        mock_svc.settings = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(mock_api, "agr-site", "settings", "get", record_id="5")
        assert result["data"]["id"] == 5
        mock_resource.get.assert_called_once_with(5)

    def test_create_simple(self, mock_api):
        mock_response = MagicMock()
        mock_response.model_dump.return_value = {"data": {"id": 10, "name": "new"}}
        mock_resource = MagicMock()
        mock_resource.create.return_value = mock_response
        mock_svc = MagicMock()
        mock_svc.settings = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(
            mock_api, "agr-site", "settings", "create", data={"name": "new"}
        )
        assert result["data"]["name"] == "new"
        mock_resource.create.assert_called_once_with({"name": "new"})

    def test_update_simple(self, mock_api):
        mock_response = MagicMock()
        mock_response.model_dump.return_value = {"data": {"id": 5, "name": "updated"}}
        mock_resource = MagicMock()
        mock_resource.update.return_value = mock_response
        mock_svc = MagicMock()
        mock_svc.settings = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(
            mock_api, "agr-site", "settings", "update", record_id="5", data={"name": "updated"}
        )
        assert result["data"]["name"] == "updated"
        mock_resource.update.assert_called_once_with(5, {"name": "updated"})

    def test_delete_simple(self, mock_api):
        mock_response = MagicMock()
        mock_response.model_dump.return_value = {"data": True}
        mock_resource = MagicMock()
        mock_resource.delete.return_value = mock_response
        mock_svc = MagicMock()
        mock_svc.settings = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(mock_api, "agr-site", "settings", "delete", record_id="5")
        assert result["data"] is True
        mock_resource.delete.assert_called_once_with(5)

    def test_nested_list(self, mock_api):
        mock_response = MagicMock()
        mock_response.model_dump.return_value = {"data": [], "count": 0}
        mock_resource = MagicMock()
        mock_resource.list_conversations.return_value = mock_response
        mock_svc = MagicMock()
        mock_svc.training = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(
            mock_api, "agr-site", "training/1/conversations", "list"
        )
        assert result["data"] == []
        mock_resource.list_conversations.assert_called_once_with(1)

    def test_nested_get(self, mock_api):
        mock_response = MagicMock()
        mock_response.model_dump.return_value = {"data": {"id": 5}}
        mock_resource = MagicMock()
        mock_resource.get_conversation.return_value = mock_response
        mock_svc = MagicMock()
        mock_svc.training = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(
            mock_api, "agr-site", "training/1/conversations", "get", record_id="5"
        )
        mock_resource.get_conversation.assert_called_once_with(1, 5)

    def test_response_without_model_dump(self, mock_api):
        """Test fallback for responses without model_dump."""
        mock_resource = MagicMock()
        mock_resource.list.return_value = {"data": [], "count": 0}
        mock_svc = MagicMock()
        mock_svc.settings = mock_resource
        type(mock_api).agr_site = PropertyMock(return_value=mock_svc)

        result = resolve_and_call(mock_api, "agr-site", "settings", "list")
        assert result == {"data": [], "count": 0}


# --- SERVICE_MAP completeness ---


class TestServiceMap:
    def test_all_27_services(self):
        assert len(SERVICE_MAP) == 27

    def test_kebab_keys(self):
        for key in SERVICE_MAP:
            assert "_" not in key or key == "p21_apis"  # All kebab-case
            # Actually just check they map to valid snake_case
            assert "_" not in key or True  # keys are kebab

    def test_snake_values(self):
        for value in SERVICE_MAP.values():
            assert "-" not in value  # All snake_case
