import pathlib
import textwrap
from unittest import mock

import httpx
import pytest
from click.testing import CliRunner
from starlette.testclient import TestClient

from fbnconfig.http_client import response_hook_async
from server.app import app


@pytest.mark.respx(base_url="https://foo.lusid.com")
class DescribeServer:

    @pytest.fixture()
    def api(self):
        client = TestClient(app)
        client.event_hooks = {"response": [response_hook_async]}
        return client

    @pytest.fixture()
    def mock_resource(self):
        class MockResource:
            @classmethod
            async def fetch(cls, client, keys):
                return {"some": "data"}

        return MockResource

    def test_homepage(self, api):
        response = api.get("/api/fbnconfig/")
        assert response.status_code == 200
        assert response.json() == {"message": "Hello, Banana!", "status": "success"}

    def test_log(self, api, respx_mock):
        # given lusid has no deployments
        respx_mock.get("/api/api/customentities/~deployment").mock(
            return_value=httpx.Response(200, json={
                "values": []
            })
        )
        # when we request them
        headers = {
            "Authorization": "Bearer BINGOBANGO",
            "X-LUSID-Host": "https://foo.lusid.com",
        }
        response = api.get("/api/fbnconfig/log/something", headers=headers)
        assert response.status_code == 200
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.headers["Authorization"] == "Bearer BINGOBANGO"
        assert request.headers["Host"] == "foo.lusid.com"

    def test_schema(self, api):
        # when we get the schema
        s = api.get("/api/fbnconfig/schema").json()
        # then it has defs and properties at the top level as expected
        assert s
        assert "$defs" in s
        assert "properties" in s

    def test_metadata(self, api):
        response = api.get("/api/fbnconfig/resources/metadata")
        assert response.status_code == 200
        assert isinstance(response.json(), list)

    def test_metadata_groups_containing_correct_resource_types(self, api):
        # If your resource is not coming back from this call you may need to add
        # your resource to __init__.py. Make sure you record your deployment.schema.json diff
        # with pytest_generate_tests as well when adding a new resource
        response = api.get("/api/fbnconfig/resources/metadata").json()
        grouped_resources = {
            group: {item["resource_type"] for item in response if item["group"] == group}
            for group in {item["group"] for item in response}
        }
        expected_grouped_resources = {
            "Identity & Access": {
                "PolicyResource",
                "PolicyCollectionResource",
                "RoleResource",
                "PolicyTemplateResource",
                "IdentityRoleResource",
                "UserResource",
                "RoleAssignment",
                "ApplicationResource"
            },
            "Compliance": {
                "ComplianceTemplateResource",
                "ComplianceRuleResource",
                "ReferenceListResource"
            },
            "Configuration Store": {
                "SetResource",
                "ItemResource",
                "SystemConfigResource"
            },
            "Portfolio Construction": {
                "CorporateActionSourceResource",
                "CustodianAccountResource",
                "TransactionTemplateResource",
                "TransactionTypeResource",
                "SideResource",
            },
            "Fund Accounting": {
                "CleardownModuleResource",
                "ChartOfAccountsResource",
                "AccountResource",
                "FundConfigurationResource",
                "GeneralLedgerResource",
                "PostingModuleResource",
            },
            "Data Model": {
                "DefinitionResource",
                "DefinitionOverrideResource",
                "IdentifierDefinitionResource",
                "DataTypeResource",
                "CustomDataModelResource",
                "EntityTypeResource",
                "EntityResource",
                "RelationalDatasetDefinitionResource",
            },
            "Cut Label Definitions": {
                "CutLabelResource",
            },
            "Drive": {
                "FolderResource",
                "FileResource",
            },
            "Horizon Integrations": {
                "IntegrationInstanceResource",
                "OptionalPropsResource",
            },
            "Luminesce": {
                "ViewResource",
                "InlinePropertiesResource",
            },
            "Notifications": {
                "SubscriptionResource",
                "NotificationResource",
            },
            "Valuation": {
                "RecipeResource",
            },
            "Scheduling": {
                "ImageResource",
                "JobResource",
                "ScheduleResource",
            },
            "Sequence": {
                "SequenceResource",
            },
            "Workflows": {
                "WorkerResource",
                "TaskDefinitionResource",
                "EventHandlerResource",
            },
            "Workspaces": {
                "WorkspaceResource",
                "WorkspaceItemResource",
            }
        }
        assert sorted(grouped_resources) == sorted(expected_grouped_resources)

    def test_metadata_resource_types_having_correct_parents(self, api):
        response = api.get("/api/fbnconfig/resources/metadata").json()
        resource_parent_map = {
            parent: {item["resource_type"] for item in response if item["parent"] == parent}
            for parent in {item["parent"] for item in response if item["parent"] is not None}
        }
        expected_resource_parent_map = {
            "ChartOfAccountsResource": {
                "AccountResource",
                "CleardownModuleResource",
                "GeneralLedgerResource",
                "PostingModuleResource",
            },
            "SetResource": {
                "ItemResource",
                "SystemConfigResource",
            },
            "EntityTypeResource": {
                "EntityResource",
            },
            "SubscriptionResource": {
                "NotificationResource",
            },
            "WorkspaceResource": {
                "WorkspaceItemResource",
            },
        }
        assert sorted(resource_parent_map) == sorted(expected_resource_parent_map)

    def test_metadata_resource_type_has_correct_schema(self, api):
        response = api.get("/api/fbnconfig/resources/metadata").json()
        policy_resource = next(
            item for item in response
            if item.get("resource_type") == "PolicyResource"
        )
        expected_policy_resource = {
            "resource_type": "PolicyResource",
            "group": "Identity & Access",
            "parent": None,
            "list_keys": [],
            "fetch_keys": ["scope", "code"],
            "filterable_columns": [],
            "supports_list": True,
            "supports_fetch": True,
            "supports_ref": True,
        }
        assert policy_resource == expected_policy_resource

    def test_metadata_resource_type_has_correct_list_keys(self, api):
        response = api.get("/api/fbnconfig/resources/metadata").json()
        workspace_item_resource = next(
            item for item in response
            if item.get("resource_type") == "WorkspaceItemResource"
        )
        expected_workspace_item_resource = {
            "resource_type": "WorkspaceItemResource",
            "group": "Workspaces",
            "parent": "WorkspaceResource",
            "list_keys": ["visibility", "name"],
            "fetch_keys": ["workspaceVisibility", "workspaceName", "group", "name"],
            "filterable_columns": [],
            "supports_list": True,
            "supports_fetch": True,
            "supports_ref": True,
        }
        assert workspace_item_resource == expected_workspace_item_resource

    @pytest.fixture()
    def example_dir(self):
        runner = CliRunner()
        with runner.isolated_filesystem():
            # given there is one example script in the examples folder
            script = textwrap.dedent("""\
                from fbnconfig import Deployment
                def configure(env):
                    return Deployment('test1', [])
            """)
            example_dir = pathlib.Path.cwd()
            with open("script.py", "w") as f:
                f.write(script)
            yield example_dir

    def test_examples(self, api, example_dir):
        # given a dir with one example script
        # and we mock the server to use it
        with mock.patch("server.app.get_examples_path", return_value=example_dir):
            # when we request examples
            examples = api.get("/api/fbnconfig/examples/").json()
            # there is one example
            assert examples == [{
                "name": "script",
                "path": "/api/fbnconfig/examples/script"
            }]

    def test_one_example(self, api, example_dir):
        # given a dir with one example script
        # and we mock the server to use it
        with mock.patch("server.app.get_examples_path", return_value=example_dir):
            # when we request an example that exists
            example = api.get("/api/fbnconfig/examples/script").json()
            # then we get a json with zero resources
            assert example == {
                "deploymentId": "test1",
                "resources": []
            }

    def test_fetch_success(self, api, mock_resource):
        # GIVEN a valid resource type
        resource_type = "SomeResource"
        # AND resource type exists in the registry
        resource_registry = {
            "SomeResource": {
                "class": mock_resource,
                "keys": ["scope", "code"]
            }
        }
        # WHEN the fetch endpoint is called for that resource type with valid keys
        with mock.patch(
            "server.api.RESOURCE_REGISTRY", resource_registry,
        ), mock.patch("server.api.fbnconfig.create_client"):
            response = api.post(
                f"/api/fbnconfig/resources/{resource_type}/fetch",
                json={
                    "keys": {
                        "scope": "some_scope",
                        "code": "some_code"
                    }
                }
            )
            print(response.json())
            # THEN the method returns 200 status code and the expected data
            assert response.status_code == 200
            assert response.json() == {"some": "data"}

    def test_fetch_error_invalid_resource_type(self, api):
        # GIVEN a resource type that doesn't match the expected camel case pattern
        resource_type = "invalid_resource"
        # WHEN the fetch endpoint is called for that resource type
        response = api.post(f"/api/fbnconfig/resources/{resource_type}/fetch")
        # THEN the method returns a 422 status code indicating validation error
        assert response.status_code == 422

    def test_fetch_error_no_such_resource(self, api):
        # GIVEN a valid resource type
        resource_type = "SomeResource"
        # AND resource type does not exist in the registry
        resource_registry = {}
        # WHEN the fetch endpoint is called for that resource type
        with mock.patch("server.api.RESOURCE_REGISTRY", resource_registry):
            response = api.post(f"/api/fbnconfig/resources/{resource_type}/fetch")
        # THEN the method returns a 404 status code indicating resource not found
        assert response.status_code == 404
        assert response.json() == {"detail": "Resource not found"}

    def test_fetch_error_fetch_method_not_implemented_on_resource(self, api):
        # GIVEN a valid resource type
        resource_type = "SomeResource"
        # AND resource type exists in the registry
        resource_registry = {
            "SomeResource": {
                # AND the resource class does not implement fetch method
                "class": mock.Mock,
                "keys": ["scope", "code"]
            }
        }
        # WHEN the fetch endpoint is called for that resource type
        with mock.patch(
            "server.api.RESOURCE_REGISTRY", resource_registry
        ):
            response = api.post(f"/api/fbnconfig/resources/{resource_type}/fetch")
            # THEN the method returns a 501 status code indicating not implemented
            assert response.status_code == 501
            assert response.json() == {"detail": "Method not implemented for this resource"}

    def test_fetch_error_missing_keys_param(self, api, mock_resource):
        # GIVEN a valid resource type
        resource_type = "SomeResource"
        # AND resource type exists in the registry
        resource_registry = {
            "SomeResource": {
                "class": mock_resource,
                "keys": ["scope", "code"]
            }
        }
        # WHEN the fetch endpoint is called for that resource type with missing required keys
        with mock.patch(
            "server.api.RESOURCE_REGISTRY", resource_registry,
        ), mock.patch("server.api.fbnconfig.create_client"):
            response = api.post(
                f"/api/fbnconfig/resources/{resource_type}/fetch",
                json={}
            )
            # THEN the method returns a 422 status code indicating validation error
            assert response.status_code == 422

    def test_fetch_error_missing_required_keys(self, api, mock_resource):
        # GIVEN a valid resource type
        resource_type = "SomeResource"
        # AND resource type exists in the registry
        resource_registry = {
            "SomeResource": {
                "class": mock_resource,
                "keys": ["scope", "code"]
            }
        }
        # WHEN the fetch endpoint is called for that resource type with missing required keys
        with mock.patch(
            "server.api.RESOURCE_REGISTRY", resource_registry,
        ), mock.patch("server.api.fbnconfig.create_client"):
            response = api.post(
                f"/api/fbnconfig/resources/{resource_type}/fetch",
                json={
                    "keys": {}
                }
            )
            # THEN the method returns a 422 status code indicating validation error
            assert response.status_code == 422

    def test_fetch_error_wrong_required_keys(self, api, mock_resource):
        # GIVEN a valid resource type
        resource_type = "SomeResource"
        # AND resource type exists in the registry
        resource_registry = {
            "SomeResource": {
                "class": mock_resource,
                "keys": ["scope", "code"]
            }
        }
        # WHEN the fetch endpoint is called for that resource type with wrong required keys
        with mock.patch(
            "server.api.RESOURCE_REGISTRY", resource_registry,
        ), mock.patch("server.api.fbnconfig.create_client"):
            response = api.post(
                f"/api/fbnconfig/resources/{resource_type}/fetch",
                json={
                    "wrong_key": "some_value"
                }
            )
            # THEN the method returns a 422 status code indicating validation error
            assert response.status_code == 422
