import datetime as dt
import json
from hashlib import sha256
from types import SimpleNamespace

import httpx
import pytest

from fbnconfig import access

TEST_BASE = "https://foo.lusid.com"


def response_hook(response: httpx.Response):
    response.read()
    response.raise_for_status()


def mock_request(mock, endpoint, response):
    mock.get(endpoint).mock(return_value=httpx.Response(200, json=response))


@pytest.mark.respx(base_url=TEST_BASE)
class DescribePolicyRef:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [response_hook]})

    @staticmethod
    def test_scope_default():
        # when scope is omitted
        sut = access.PolicyRef(id="one", code="cd")
        # then it uses "default"
        assert sut.scope == "default"

    def test_attach_when_present(self, respx_mock):
        # given that the remote definition exists
        respx_mock.get("/access/api/policies/pol1?scope=sc1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        sut = access.PolicyRef(id="one", scope="sc1", code="pol1")
        # when we call attach
        sut.attach(client)
        # then a get request was made and no exception raised

    def test_attach_when_missing(self, respx_mock):
        # given the remote does not exist
        respx_mock.get("/access/api/policies/pol1?scope=sc1").mock(
            return_value=httpx.Response(404, json={})
        )
        client = self.client
        sut = access.PolicyRef(id="one", scope="sc1", code="pol1")
        # when we call attach an exception is raised
        with pytest.raises(RuntimeError) as ex:
            sut.attach(client)
        assert "Policy sc1/pol1 not found" in str(ex.value)

    def test_attach_when_http_error(self, respx_mock):
        # given the server returns an error
        respx_mock.get("/access/api/policies/pol1?scope=sc1").mock(
            return_value=httpx.Response(500, json={})
        )
        client = self.client
        sut = access.PolicyRef(id="one", scope="sc1", code="pol1")
        # when we call attach an exception is raised
        with pytest.raises(httpx.HTTPError):
            sut.attach(client)


@pytest.mark.respx(base_url=TEST_BASE)
class DescribePolicy:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [response_hook]})

    @pytest.fixture
    def stock_policy_resource(self):
        # uses new style selector definition: List[Selector]
        return access.PolicyResource(
            id="policy1",
            code="a_policy",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="banjo",
                    description="whatever",
                    identifier={"scope": "w", "code": "y"},
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )

    @pytest.fixture
    def policy_resource(self):
        return {
            "values": [
                {
                    "id": {
                        "scope": "default",
                        "code": "policy1"
                    },
                    "description": "policy description",
                    "applications": ["lusid"],
                    "grant": "Allow",
                    "selectors": [
                        {
                            "idSelectorDefinition": {
                                "identifier": {"scope": "w", "code": "y"},
                                "actions": [
                                    {
                                        "scope": "actscope",
                                        "activity": "execute",
                                        "entity": "Feature"
                                    }
                                ],
                                "name": "banjo",
                                "description": "whatever"
                            }
                        }
                    ],
                    "when": {
                        "activate": "2024-08-31T18:00:00.0000000+00:00"
                    },
                    "links": {"self": "some-link"},
                }
            ],
            "links": {"self": "some-link"},
            "href": "some-href"
        }

    def test_create(self, respx_mock):
        respx_mock.post("/access/api/policies").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # given no policies exist yet
        # when we create one
        sut = access.PolicyResource(
            id="policy1",
            code="a_policy",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="banjo",
                    description="whatever",
                    identifier={"scope": "w", "code": "y"},
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )
        state = sut.create(client)
        # then the state is returned
        assert state["id"] == "policy1"
        assert state["code"] == "a_policy"
        # and a create request was sent
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/policies"
        assert json.loads(request.content) == {
            "applications": ["lusid"],
            "code": "a_policy",
            "description": "policy description",
            "grant": "Allow",
            "selectors": [
                {
                    "idSelectorDefinition": {
                        "actions": [{"activity": "execute", "entity": "Feature", "scope": "actscope"}],
                        "description": "whatever",
                        "identifier": {"code": "y", "scope": "w"},
                        "name": "banjo",
                    }
                }
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }

    def test_parse_api_format(self):
        # given an api format as returned by /access/api/policy
        remote = {
            "applications": ["lusid"],
            "id": {
                "code": "a_policy",
                "scope": "a_scope",
            },
            "description": "policy description",
            "grant": "Allow",
            "selectors": [
                {
                    "idSelectorDefinition": {
                        "actions": [{"activity": "execute", "entity": "Feature", "scope": "actscope"}],
                        "description": "whatever",
                        "identifier": {"code": "y", "scope": "w"},
                        "name": "banjo",
                    }
                }
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }
        # when we push it through the deserialization
        converted = access.PolicyResource.model_validate(remote, context={"id": "res123"})
        # then
        assert converted.id == "res123"  # the resource id is from the context
        assert converted.applications == ["lusid"]
        assert converted.scope == "a_scope"  # scope and code get lifted to the top level
        assert converted.code == "a_policy"
        assert type(converted.selectors[0]).__name__ == "IdSelector"  # selector type inferred
        assert converted.selectors[0].type_name == "idSelectorDefinition"
        assert converted.when.activate == "2024-08-31T18:00:00.0000000+00:00"

    def test_dump_policy_resource(self):
        # given a policy resource
        sut = access.PolicyResource(
            id="dump-policy",
            code="DumpPolicy",
            description="A test policy for dumping",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="test-selector",
                    description="Test ID selector",
                    identifier={"scope": "test-scope", "code": "test-code"},
                    actions=[access.ActionId(scope="test", activity="execute", entity="Feature")],
                )
            ],
        )
        # when we dump it
        result = sut.model_dump(
            mode="json", by_alias=True, round_trip=True, exclude_none=True, context={"style": "dump"}
        )
        # then all fields are included with proper aliases
        expected = {
            "code": "DumpPolicy",
            "description": "A test policy for dumping",
            "applications": ["lusid"],
            "grant": "Allow",
            "selectors": [
                {
                    "typeName": "idSelectorDefinition",
                    "identifier": {"scope": "test-scope", "code": "test-code"},
                    "actions": [{"scope": "test", "activity": "execute", "entity": "Feature"}],
                    "name": "test-selector",
                    "description": "Test ID selector",
                }
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }
        assert result == expected

    def test_undump_policy_resource(self):
        # given dumped policy data
        data = {
            "code": "UndumpPolicy",
            "description": "A test policy for undumping",
            "applications": ["lusid"],
            "grant": "Allow",
            "selectors": [
                {
                    "typeName": "idSelectorDefinition",
                    "identifier": {"scope": "undump-scope", "code": "undump-code"},
                    "actions": [{"scope": "undump", "activity": "read", "entity": "Portfolio"}],
                    "name": "undump-selector",
                    "description": "Undump ID selector",
                }
            ],
            "when": {"activate": "2024-12-01T00:00:00.0000000+00:00"},
        }
        # when we undump it
        result = access.PolicyResource.model_validate(data, context={
            "style": "dump", "id": "undump-policy"
        })
        # then the resource Id is extracted from the context
        assert result.id == "undump-policy"
        assert result.code == "UndumpPolicy"
        # and the selector has the right type
        assert len(result.selectors) == 1
        assert isinstance(result.selectors[0], access.IdSelector)
        assert result.selectors[0].type_name == "idSelectorDefinition"
        assert result.when.activate == "2024-12-01T00:00:00.0000000+00:00"

    def test_create_matchall_selector(self, respx_mock):
        respx_mock.post("/access/api/policies").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # given no policies exist yet
        # when we create one with a matchall selector
        sut = access.PolicyResource(
            id="policy1",
            code="a_policy",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="banjo",
                    description="whatever",
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )
        state = sut.create(client)
        # then the state is returned
        assert state["id"] == "policy1"
        assert state["code"] == "a_policy"
        # and a create request was sent
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/policies"
        assert json.loads(request.content) == {
            "applications": ["lusid"],
            "code": "a_policy",
            "description": "policy description",
            "grant": "Allow",
            "selectors": [
                {
                    "matchAllSelectorDefinition": {
                        "actions": [{"activity": "execute", "entity": "Feature", "scope": "actscope"}],
                        "description": "whatever",
                        "name": "banjo",
                    }
                }
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }

    def test_create_metadata_selector(self, respx_mock):
        respx_mock.post("/access/api/policies").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # given no policies exist yet
        # when we create one with a metadata selector
        sut = access.PolicyResource(
            id="policy1",
            code="a_policy",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MetadataSelector(
                    name="banjo",
                    description="whatever",
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                    expressions=[
                        access.MetadataExpression(
                            metadata_key="foo", operator="Equals", text_value="bingo"
                        )
                    ],
                )
            ],
        )
        state = sut.create(client)
        # then the state is returned
        assert state["id"] == "policy1"
        assert state["code"] == "a_policy"
        # and a create request was sent
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/policies"
        assert json.loads(request.content) == {
            "applications": ["lusid"],
            "code": "a_policy",
            "description": "policy description",
            "grant": "Allow",
            "selectors": [
                {
                    "metadataSelectorDefinition": {
                        "actions": [{"activity": "execute", "entity": "Feature", "scope": "actscope"}],
                        "description": "whatever",
                        "name": "banjo",
                        "expressions": [
                            {"metadataKey": "foo", "operator": "Equals", "textValue": "bingo"}
                        ],
                    }
                }
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }

    def test_create_policy_selector(self, respx_mock):
        respx_mock.post("/access/api/policies").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # given no policies exist yet
        # when we create one with a policy selector and a nested matchall
        inner_selector = access.MatchAllSelector(
            name="banjo",
            description="whatever",
            actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
        )
        sut = access.PolicyResource(
            id="policy1",
            code="a_policy",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.PolicySelector(
                    name="banjo",
                    description="whatever",
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                    identity_restriction={"id": "value"},
                    restriction_selectors=[inner_selector],
                )
            ],
        )
        state = sut.create(client)
        # then the state is returned
        assert state["id"] == "policy1"
        assert state["code"] == "a_policy"
        # and a create request was sent including the inner selector
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/policies"
        assert json.loads(request.content) == {
            "applications": ["lusid"],
            "code": "a_policy",
            "description": "policy description",
            "grant": "Allow",
            "selectors": [
                {
                    "policySelectorDefinition": {
                        "actions": [{"activity": "execute", "entity": "Feature", "scope": "actscope"}],
                        "description": "whatever",
                        "name": "banjo",
                        "identityRestriction": {"id": "value"},
                        "restrictionSelectors": [
                            {
                                "matchAllSelectorDefinition": {
                                    "actions": [
                                        {"activity": "execute", "entity": "Feature", "scope": "actscope"}
                                    ],
                                    "description": "whatever",
                                    "name": "banjo",
                                }
                            }
                        ],
                    }
                }
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }

    @pytest.fixture
    def read_policy_response(self):
        return {
            "applications": ["lusid"],
            "id": {
                "code": "a_policy_code",
                "scope": "default",
            },  # get returns an ID where post takes a code only
            "description": "policy description",
            "grant": "Allow",
            "selectors": [
                {
                    "idSelectorDefinition": {
                        "actions": [{"activity": "execute", "entity": "Feature", "scope": "actscope"}],
                        "description": "whatever",
                        "identifier": {"code": "y", "scope": "w"},
                        "name": "banjo",
                    }
                }
            ],
            "when": {
                "activate": "2024-08-31T18:00:00.0000000+00:00",
                "deactivate": "99999-12-31T18:00:00.0000000+00:00",
            },
            "links": {},
        }

    def test_update_no_changes(self, respx_mock, read_policy_response):
        # given the policy exists
        respx_mock.get("/access/api/policies/a_policy_code").mock(
            side_effect=[httpx.Response(200, json=read_policy_response)]
        )
        # and the desired is the same as the existing
        sut = access.PolicyResource(
            id="policy1",
            code="a_policy_code",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="banjo",
                    description="whatever",
                    identifier={"scope": "w", "code": "y"},
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )
        # when we update
        log = sut.update(self.client, SimpleNamespace(code="a_policy_code", id="x"))
        # then there is no change and the log remains as is
        assert log is None
        # and no put request was made

    def test_update_modify_description(self, respx_mock, read_policy_response):
        # given the policy exists with a description = "policy description"
        respx_mock.get("/access/api/policies/a_policy_code").mock(
            side_effect=[httpx.Response(200, json=read_policy_response)]
        )
        respx_mock.put("/access/api/policies/a_policy_code").mock(
            side_effect=[httpx.Response(200, json={})]
        )
        # and the desired has a different description
        sut = access.PolicyResource(
            id="policy1",
            code="a_policy_code",
            description="a different policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="banjo",
                    description="whatever",
                    identifier={"scope": "w", "code": "y"},
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )
        # when we update
        log = sut.update(self.client, SimpleNamespace(code="a_policy_code", id="x"))
        # the log is returned (but unchanged)
        assert log == {"code": "a_policy_code", "id": "policy1"}
        # and a put request was made to update the policy
        request = respx_mock.calls.last.request
        assert request.method == "PUT"
        assert request.url.path == "/access/api/policies/a_policy_code"
        assert json.loads(request.content) == {
            "applications": ["lusid"],
            "description": "a different policy description",
            "grant": "Allow",
            "selectors": [
                {
                    "idSelectorDefinition": {
                        "actions": [{"activity": "execute", "entity": "Feature", "scope": "actscope"}],
                        "description": "whatever",
                        "identifier": {"code": "y", "scope": "w"},
                        "name": "banjo",
                    }
                }
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }

    def test_deps(self, stock_policy_resource):
        # policy doesn't have any deps
        sut = stock_policy_resource
        assert sut.deps() == []

    def test_delete(self, respx_mock):
        respx_mock.delete("/access/api/policies/a_policy_code").mock(
            side_effect=[httpx.Response(200, json={})]
        )
        # given an existing policy
        # when we delete it
        old_state = SimpleNamespace(id="policy1", code="a_policy_code")
        access.PolicyResource.delete(self.client, old_state)
        # then a delete request is made
        request = respx_mock.calls.last.request
        assert request.method == "DELETE"
        assert request.url.path == "/access/api/policies/a_policy_code"

    def test_from_api(self):
        resp = {
            "id": {
                "scope": "DEFAULT",
                "code": "AaAa"
            },
            "description": "",
            "applications": ["LUSID"],
            "grant": "Allow",
            "selectors": [
                {
                    "idSelectorDefinition": {
                        "identifier": {
                            "scope": "ada"
                        },
                        "actions": [
                            {
                                "scope": "default",
                                "activity": "Read", "entity": "Instrument"
                            },
                            {
                                "scope": "default",
                                "activity": "Upsert",
                                "entity": "Instrument"
                            }
                        ]
                    }
                }
            ],
            "when": {
                "activate": "2024-05-07T23:00:00.0000000+00:00",
                "deactivate": "9999-12-31T23:59:59.9999999+00:00"
            }
        }
        policy_data = access.PolicyResource.from_api(resp)
        instance = access.PolicyResource(**policy_data)
        assert instance.id == "policy_default_aaaa"
        assert instance.scope == "DEFAULT"
        assert instance.code == "AaAa"

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_list(self, respx_mock, policy_resource):
        # GIVEN expected result structure
        expected_result = {
            "values": [
                {
                    "value": {
                        "id": "policy_default_policy1",
                        "description": "policy description",
                        "applications": ["lusid"],
                        "grant": "Allow",
                        "selectors": [
                            {
                                "idSelectorDefinition": {
                                    "identifier": {
                                        "scope": "w",
                                        "code": "y"},
                                    "actions": [
                                        {
                                            "scope": "actscope",
                                            "activity": "execute",
                                            "entity": "Feature"
                                        }
                                    ],
                                    "name": "banjo",
                                    "description": "whatever"
                                }
                            }
                        ],
                        "when": {
                            "activate": "2024-08-31T18:00:00.0000000+00:00"
                        },
                        "scope": "default", "code": "policy1"},
                    "keys": {
                        "scope": "default",
                        "code": "policy1"
                    }
                }
            ]
        }
        # AND a mocked list endpoint
        respx_mock.get(
            "/access/api/policies/page"
        ).mock(
            return_value=httpx.Response(200, json=policy_resource)
        )

        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        # WHEN calling the list method with query params
        result = await access.PolicyResource.list(
            async_client,
            query_params={
                "limit": 50,
                "filter": "test",
                "page": "next_page"
            }
        )

        # THEN the result excludes internal fields
        assert "links" not in result
        assert "href" not in result

        # AND contains the expected values
        assert "values" in result
        assert len(result["values"]) == 1
        assert result == expected_result
        result = access.PolicyResource.model_validate(result["values"][0]["value"])
        assert result.id == "policy_default_policy1"
        assert result.scope == "default"
        assert result.code == "policy1"

        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == "/access/api/policies/page"
        assert request.url.query == b"limit=50&filter=test&page=next_page"

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_fetch(self, respx_mock, policy_resource):
        # GIVEN a Policy resource with specific scope and code
        resource = policy_resource["values"][0]
        scope = resource["id"]["scope"]
        code = resource["id"]["code"]
        # AND the resource exists in LUSID
        respx_mock.get(
            f"/access/api/policies/{code}"
        ).mock(
            return_value=httpx.Response(200, json=resource)
        )
        # WHEN the fetch method is called for that resource
        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        response = await access.PolicyResource.fetch(
            async_client,
            keys={"scope": scope, "code": code}
        )
        # THEN the response is as expected
        assert response == {
            "id": f"policy_{scope}_{code}",
            "scope": scope,
            "code": code,
            "description": resource["description"],
            "applications": resource["applications"],
            "grant": resource["grant"],
            "selectors": resource["selectors"],
            "when": resource["when"],
        }
        # AND the correct request was made to LUSID
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == f"/access/api/policies/{code}"
        assert request.url.query == b"scope=default"

    def test_create_with_for_and_if(self, respx_mock):
        respx_mock.post("/access/api/policies").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        sut = access.PolicyResource(
            id="policy_for_if",
            code="for_if_policy",
            description="policy with for and if",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="all",
                    actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
                )
            ],
            for_spec=[
                access.EffectiveRange(
                    from_="2015-12-25T00:00:00.0000000+00:00", to="2020-12-25T00:00:00.0000000+00:00"
                )
            ],
            if_spec=[
                access.IfRequestHeaderExpression(
                    header_name="organisation.specific.group.header",
                    operator=access.IfOperatorEnum.EQUALSCASEINSENSITIVE,
                    value="special-group",
                )
            ],
        )
        state = sut.create(client)
        assert state == {"id": "policy_for_if", "code": "for_if_policy"}
        body = json.loads(respx_mock.calls.last.request.content)
        assert body["for"] == [
            {
                "effectiveRange": {
                    "from": "2015-12-25T00:00:00+00:00",
                    "to": "2020-12-25T00:00:00+00:00",
                }
            }
        ]
        assert body["if"] == [
            {
                "ifRequestHeaderExpression": {
                    "headerName": "organisation.specific.group.header",
                    "operator": "EqualsCaseInsensitive",
                    "value": "special-group",
                }
            }
        ]

    def test_create_without_for_and_if(self, respx_mock):
        respx_mock.post("/access/api/policies").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        sut = access.PolicyResource(
            id="policy_no_for_if",
            code="no_for_if",
            description="policy without for/if",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="all",
                    actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
                )
            ],
        )
        sut.create(client)
        body = json.loads(respx_mock.calls.last.request.content)
        assert "for" not in body
        assert "if" not in body

    def test_update_no_change_with_for_and_if(self, respx_mock):
        read_response = {
            "applications": ["lusid"],
            "id": {"code": "for_if_code", "scope": "default"},
            "description": "with for and if",
            "grant": "Allow",
            "selectors": [
                {
                    "matchAllSelectorDefinition": {
                        "actions": [{"scope": "default", "activity": "Read", "entity": "Portfolio"}],
                        "name": "all",
                    }
                }
            ],
            "for": [
                {
                    "effectiveRange": {
                        "from": "2015-12-25T00:00:00.0000000+00:00",
                        "to": "2020-12-25T00:00:00.0000000+00:00",
                    }
                }
            ],
            "if": [
                {
                    "ifRequestHeaderExpression": {
                        "headerName": "X-Group",
                        "operator": "EqualsCaseInsensitive",
                        "value": "special",
                    }
                }
            ],
            "when": {
                "activate": "2024-08-31T18:00:00.0000000+00:00",
                "deactivate": "99999-12-31T18:00:00.0000000+00:00",
            },
            "links": {},
        }
        respx_mock.get("/access/api/policies/for_if_code").mock(
            return_value=httpx.Response(200, json=read_response)
        )
        sut = access.PolicyResource(
            id="policy_for_if",
            code="for_if_code",
            description="with for and if",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="all",
                    actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
                )
            ],
            for_spec=[
                access.EffectiveRange(
                    from_="2015-12-25T00:00:00.0000000+00:00", to="2020-12-25T00:00:00.0000000+00:00"
                )
            ],
            if_spec=[
                access.IfRequestHeaderExpression(
                    header_name="X-Group",
                    operator=access.IfOperatorEnum.EQUALSCASEINSENSITIVE,
                    value="special"
                )
            ],
        )
        log = sut.update(self.client, SimpleNamespace(code="for_if_code", id="x"))
        assert log is None

    def test_update_modify_for(self, respx_mock):
        read_response = {
            "applications": ["lusid"],
            "id": {"code": "for_mod_code", "scope": "default"},
            "description": "desc",
            "grant": "Allow",
            "selectors": [
                {
                    "matchAllSelectorDefinition": {
                        "actions": [{"scope": "default", "activity": "Read", "entity": "Portfolio"}],
                        "name": "all",
                    }
                }
            ],
            "for": [
                {
                    "effectiveRange": {
                        "from": "2015-01-01T00:00:00+00:00",
                        "to": "2020-01-01T00:00:00+00:00",
                    }
                }
            ],
            "when": {
                "activate": "2024-08-31T18:00:00.0000000+00:00",
                "deactivate": "99999-12-31T18:00:00.0000000+00:00",
            },
            "links": {},
        }
        respx_mock.get("/access/api/policies/for_mod_code").mock(
            return_value=httpx.Response(200, json=read_response)
        )
        respx_mock.put("/access/api/policies/for_mod_code").mock(
            return_value=httpx.Response(200, json={})
        )
        sut = access.PolicyResource(
            id="policy_for_mod",
            code="for_mod_code",
            description="desc",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="all",
                    actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
                )
            ],
            for_spec=[
                access.EffectiveRange(from_="2022-01-01T00:00:00+00:00", to="2025-01-01T00:00:00+00:00")
            ],
        )
        log = sut.update(self.client, SimpleNamespace(code="for_mod_code", id="x"))
        assert log == {"id": "policy_for_mod", "code": "for_mod_code"}
        request = respx_mock.calls.last.request
        assert request.method == "PUT"
        body = json.loads(request.content)
        assert body["for"] == [
            {"effectiveRange": {"from": "2022-01-01T00:00:00+00:00", "to": "2025-01-01T00:00:00+00:00"}}
        ]

    def test_dump_undump_with_for_and_if(self):
        sut = access.PolicyResource(
            id="dump-for-if",
            code="DumpForIf",
            description="round-trip test",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="sel1",
                    description="test",
                    identifier={"scope": "s", "code": "c"},
                    actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
                )
            ],
            for_spec=[
                access.EffectiveRange(
                    from_="2015-12-25T00:00:00.0000000+00:00",
                    to="2020-12-25T00:00:00.0000000+00:00"
                ),
                access.AsAtRangeForSpec(
                    from_=access.AsAtPredicateContract(date_time_offset="2018-01-01T00:00:00.0000000+00:00"),
                    to=access.AsAtPredicateContract(value="AsAt=Latest"),
                ),
            ],
            if_spec=[
                access.IfRequestHeaderExpression(
                    header_name="X-Group",
                    operator=access.IfOperatorEnum.EQUALSCASEINSENSITIVE,
                    value="special-group"
                )
            ],
        )

        dumped = sut.model_dump(
            mode="json", by_alias=True, round_trip=True, exclude_none=True, context={"style": "dump"}
        )
        assert "for" in dumped
        assert len(dumped["for"]) == 2
        assert dumped["for"][0]["typeName"] == "effectiveRange"
        assert dumped["for"][1]["typeName"] == "asAtRangeForSpec"
        assert "if" in dumped
        assert len(dumped["if"]) == 1
        assert dumped["if"][0]["typeName"] == "ifRequestHeaderExpression"

        restored = access.PolicyResource.model_validate(
            dumped, context={"style": "dump", "id": "dump-for-if"}
        )
        assert restored.id == "dump-for-if"
        assert restored.for_spec is not None
        assert len(restored.for_spec) == 2
        assert isinstance(restored.for_spec[0], access.EffectiveRange)
        assert restored.for_spec[0].from_ == dt.datetime.fromisoformat("2015-12-25T00:00:00+00:00")
        assert isinstance(restored.for_spec[1], access.AsAtRangeForSpec)
        assert restored.for_spec[1].to.value == "AsAt=Latest"
        assert restored.if_spec is not None
        assert len(restored.if_spec) == 1
        assert isinstance(restored.if_spec[0], access.IfRequestHeaderExpression)
        assert restored.if_spec[0].header_name == "X-Group"

    def test_from_api_includes_for_and_if(self):
        resp = {
            "id": {"scope": "DEFAULT", "code": "WithForIf"},
            "description": "has for and if",
            "applications": ["LUSID"],
            "grant": "Allow",
            "selectors": [
                {
                    "idSelectorDefinition": {
                        "identifier": {"scope": "s"},
                        "actions": [{"scope": "default", "activity": "Read", "entity": "Instrument"}],
                    }
                }
            ],
            "for": [
                {
                    "effectiveRange": {
                        "from": "2015-12-25T00:00:00.0000000+00:00",
                        "to": "2020-12-25T00:00:00.0000000+00:00",
                    }
                }
            ],
            "if": [
                {
                    "ifRequestHeaderExpression": {
                        "headerName": "X-Group",
                        "operator": "EqualsCaseInsensitive",
                        "value": "special",
                    }
                }
            ],
            "when": {"activate": "2024-05-07T23:00:00.0000000+00:00"},
        }
        policy_data = access.PolicyResource.from_api(resp)
        assert "for" in policy_data
        assert "if" in policy_data
        instance = access.PolicyResource(**policy_data)
        assert instance.id == "policy_default_withforif"
        assert instance.for_spec is not None
        assert len(instance.for_spec) == 1
        assert isinstance(instance.for_spec[0], access.EffectiveRange)
        assert instance.if_spec is not None
        assert len(instance.if_spec) == 1
        assert isinstance(instance.if_spec[0], access.IfRequestHeaderExpression)

    def test_parse_api_format_with_for_and_if(self):
        remote = {
            "applications": ["lusid"],
            "id": {"code": "a_policy", "scope": "a_scope"},
            "description": "policy description",
            "grant": "Allow",
            "selectors": [
                {
                    "idSelectorDefinition": {
                        "actions": [{"activity": "execute", "entity": "Feature", "scope": "actscope"}],
                        "description": "whatever",
                        "identifier": {"code": "y", "scope": "w"},
                        "name": "banjo",
                    }
                }
            ],
            "for": [
                {
                    "effectiveRange": {
                        "from": "2015-12-25T00:00:00.0000000+00:00",
                        "to": "2020-12-25T00:00:00.0000000+00:00",
                    }
                },
                {
                    "asAtRangeForSpec": {
                        "from": {"dateTimeOffset": "2018-01-01T00:00:00.0000000+00:00"},
                        "to": {"value": "AsAt=Latest"},
                    }
                },
            ],
            "if": [
                {
                    "ifRequestHeaderExpression": {
                        "headerName": "X-Group",
                        "operator": "EqualsCaseInsensitive",
                        "value": "special",
                    }
                }
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }
        converted = access.PolicyResource.model_validate(remote, context={"id": "res123"})
        assert converted.for_spec is not None
        assert len(converted.for_spec) == 2
        assert isinstance(converted.for_spec[0], access.EffectiveRange)
        assert converted.for_spec[0].from_ == dt.datetime.fromisoformat("2015-12-25T00:00:00+00:00")
        assert converted.for_spec[0].to == dt.datetime.fromisoformat("2020-12-25T00:00:00+00:00")
        assert isinstance(converted.for_spec[1], access.AsAtRangeForSpec)
        assert converted.for_spec[1].from_.date_time_offset == dt.datetime.fromisoformat(
            "2018-01-01T00:00:00+00:00"
        )
        assert converted.for_spec[1].to.value == "AsAt=Latest"
        assert converted.if_spec is not None
        assert len(converted.if_spec) == 1
        assert isinstance(converted.if_spec[0], access.IfRequestHeaderExpression)
        assert converted.if_spec[0].header_name == "X-Group"
        assert converted.if_spec[0].operator == "EqualsCaseInsensitive"
        assert converted.if_spec[0].value == "special"

    def test_update_no_change_server_returns_null_for_if(self, respx_mock):
        read_response = {
            "applications": ["lusid"],
            "id": {"code": "null_for_if", "scope": "default"},
            "description": "no for/if",
            "grant": "Allow",
            "selectors": [
                {
                    "matchAllSelectorDefinition": {
                        "actions": [{"scope": "default", "activity": "Read", "entity": "Portfolio"}],
                        "name": "all",
                    }
                }
            ],
            "for": None,
            "if": None,
            "when": {
                "activate": "2024-08-31T18:00:00.0000000+00:00",
                "deactivate": "99999-12-31T18:00:00.0000000+00:00",
            },
            "links": {},
        }
        respx_mock.get("/access/api/policies/null_for_if").mock(
            return_value=httpx.Response(200, json=read_response)
        )
        sut = access.PolicyResource(
            id="policy_null",
            code="null_for_if",
            description="no for/if",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="all",
                    actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
                )
            ],
        )
        log = sut.update(self.client, SimpleNamespace(code="null_for_if", id="x"))
        assert log is None

    def test_create_with_all_for_spec_types(self, respx_mock):
        respx_mock.post("/access/api/policies").mock(return_value=httpx.Response(200, json={}))
        sut = access.PolicyResource(
            id="p",
            code="all_for",
            description="all for spec types",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="all",
                    actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
                )
            ],
            for_spec=[
                access.EffectiveRange(
                    from_="2015-12-25T00:00:00.0000000+00:00",
                    to="2020-12-25T00:00:00.0000000+00:00"
                ),
                access.AsAtRangeForSpec(
                    from_=access.AsAtPredicateContract(date_time_offset="2018-01-01T00:00:00.0000000+00:00"),
                    to=access.AsAtPredicateContract(value="AsAt=Latest"),
                ),
                access.EffectiveDateHasQuality(quality=access.EffectiveDateHasQualityEnum.ISFIRSTDAYOFANYMONTH),
                access.EffectiveDateRelative(
                    date=access.RelativeDateEnum.NOW,
                    adjustment=1,
                    unit=access.RelativeUnitEnum.DAY,
                    relative_to_date_time=access.RelativeToDateTimeEnum.BEFOREORON
                ),
                access.AsAtRelative(
                    date=access.RelativeDateEnum.FIRSTOFMONTH,
                    adjustment=0,
                    unit=access.RelativeUnitEnum.MONTHS,
                    relative_to_date_time=access.RelativeToDateTimeEnum.ONDAYOF
                ),
            ],
        )
        sut.create(self.client)
        body = json.loads(respx_mock.calls.last.request.content)
        assert body["for"] == [
            {"effectiveRange": {
                "from": "2015-12-25T00:00:00+00:00",
                "to": "2020-12-25T00:00:00+00:00"
            }},
            {"asAtRangeForSpec": {
                "from": {"dateTimeOffset": "2018-01-01T00:00:00+00:00"},
                "to": {"value": "AsAt=Latest"}
            }},
            {"effectiveDateHasQuality": {
                "quality": "IsFirstDayOfAnyMonth"
            }},
            {"effectiveDateRelative": {
                "date": "Now",
                "adjustment": 1,
                "unit": "Day",
                "relativeToDateTime": "BeforeOrOn"
            }},
            {"asAtRelative": {
                "date": "FirstOfMonth",
                "adjustment": 0,
                "unit": "Months",
                "relativeToDateTime": "OnDayOf"
            }},
        ]

    def test_create_with_all_if_spec_types(self, respx_mock):
        respx_mock.post("/access/api/policies").mock(return_value=httpx.Response(200, json={}))
        sut = access.PolicyResource(
            id="p",
            code="all_if",
            description="all if spec types",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="all",
                    actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
                )
            ],
            if_spec=[
                access.IfRequestHeaderExpression(
                    header_name="X-Header",
                    operator=access.IfOperatorEnum.EQUALSCASEINSENSITIVE,
                    value="val"
                ),
                access.IfIdentityClaimExpression(
                    claim_type="group",
                    operator=access.IfOperatorEnum.NOTEQUALSCASESENSITIVE,
                    value="admin"
                ),
                access.IfIdentityScopeExpression(scope_name="default"),
                access.IfViaApiExpression(api_feature_codes="SomeCode"),
            ],
        )
        sut.create(self.client)
        body = json.loads(respx_mock.calls.last.request.content)
        assert body["if"] == [
            {"ifRequestHeaderExpression": {
                "headerName": "X-Header",
                "operator": "EqualsCaseInsensitive",
                "value": "val"
            }},
            {"ifIdentityClaimExpression": {
                "claimType": "group",
                "operator": "NotEqualsCaseSensitive",
                "value": "admin"
            }},
            {"ifIdentityScopeExpression": {"scopeName": "default"}},
            {"ifViaApiExpression": {"apiFeatureCodes": "SomeCode"}},
        ]

    def test_parse_api_with_all_spec_types(self):
        remote = {
            "applications": ["lusid"],
            "id": {"code": "all_specs", "scope": "default"},
            "description": "all types",
            "grant": "Allow",
            "selectors": [{"matchAllSelectorDefinition": {
                "actions": [{"scope": "default", "activity": "Read", "entity": "Portfolio"}],
                "name": "all",
            }}],
            "for": [
                {"effectiveDateHasQuality": {
                    "quality": access.EffectiveDateHasQualityEnum.ISFIRSTDAYOFANYMONTH
                }},
                {"effectiveDateRelative": {
                    "date": "Now",
                    "adjustment": 1,
                    "unit": "Day",
                    "relativeToDateTime": access.RelativeToDateTimeEnum.BEFOREORON
                }},
                {"asAtRelative": {
                    "date": "FirstOfMonth",
                    "adjustment": 0,
                    "unit": "Months",
                    "relativeToDateTime": access.RelativeToDateTimeEnum.ONDAYOF
                }},
            ],
            "if": [
                {"ifIdentityClaimExpression": {
                    "claimType": "group",
                    "value": "admin",
                    "operator": access.IfOperatorEnum.NOTPRESENT
                    }},
                {"ifIdentityScopeExpression": {"scopeName": "default"}},
                {"ifViaApiExpression": {"apiFeatureCodes": "SomeCode"}},
            ],
            "when": {"activate": "2024-08-31T18:00:00.0000000+00:00"},
        }
        policy = access.PolicyResource.model_validate(remote, context={"id": "all-specs"})
        assert isinstance(policy.for_spec, list)
        assert len(policy.for_spec) == 3
        assert isinstance(policy.for_spec[0], access.EffectiveDateHasQuality)
        assert policy.for_spec[0].quality == "IsFirstDayOfAnyMonth"
        assert isinstance(policy.for_spec[1], access.EffectiveDateRelative)
        assert policy.for_spec[1].date == "Now"
        assert policy.for_spec[1].adjustment == 1
        assert policy.for_spec[1].relative_to_date_time == "BeforeOrOn"
        assert isinstance(policy.for_spec[2], access.AsAtRelative)
        assert policy.for_spec[2].date == "FirstOfMonth"
        assert policy.for_spec[2].adjustment == 0
        assert isinstance(policy.if_spec, list)
        assert len(policy.if_spec) == 3
        assert isinstance(policy.if_spec[0], access.IfIdentityClaimExpression)
        assert policy.if_spec[0].claim_type == "group"
        assert isinstance(policy.if_spec[1], access.IfIdentityScopeExpression)
        assert policy.if_spec[1].scope_name == "default"
        assert isinstance(policy.if_spec[2], access.IfViaApiExpression)
        assert policy.if_spec[2].api_feature_codes == "SomeCode"
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [response_hook]})

    def test_attach_when_present(self, respx_mock):
        # given that the remote definition exists
        respx_mock.get("/access/api/roles/cd1?scope=sc1").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        sut = access.RoleRef(id="one", scope="sc1", code="cd1")
        # when we call attach
        sut.attach(client)
        # then a get request was made and no exception raised

    def test_attach_when_missing(self, respx_mock):
        # given the remote does not exist
        respx_mock.get("/access/api/roles/cd1?scope=default").mock(
            return_value=httpx.Response(404, json={})
        )
        client = self.client
        sut = access.RoleRef(id="one", code="cd1")
        # when we call attach an exception is raised
        with pytest.raises(RuntimeError) as ex:
            sut.attach(client)
        assert "Role default/cd1 not found" in str(ex.value)

    def test_attach_when_http_error(self, respx_mock):
        # given the server returns an error
        respx_mock.get("/access/api/roles/cd1?scope=sc1").mock(return_value=httpx.Response(500, json={}))
        client = self.client
        sut = access.RoleRef(id="one", scope="sc1", code="cd1")
        # when we call attach an exception is raised
        with pytest.raises(httpx.HTTPError):
            sut.attach(client)


@pytest.mark.respx(base_url=TEST_BASE)
class DescribeRole:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [response_hook]})

    @pytest.fixture
    def stock_policy_resource(self):
        return access.PolicyResource(
            id="policy1",
            code="a_policy",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="banjo",
                    description="whatever",
                    identifier={"scope": "w", "code": "y"},
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )

    @pytest.fixture
    def role_resource(self):
        return [
            {
                "id": {
                    "scope": "default",
                    "code": "testid68487f04a1f047fb864187e6594abd4f_-+"
                },
                "roleHierarchyIndex": 2216,
                "description": "TestRole",
                "resource": {
                    "policyIdRoleResource": {
                        "policies": [
                            {
                                "scope": "LUSID_SYSTEM",
                                "code": "testid68487f04a1f047fb864187e6594abd4f_-+"
                            }
                        ],
                        "policyCollections": [
                            {
                                "scope": "LUSID_SYSTEM",
                                "code": "testid68487f04a1f047fb864187e6594abd4f_-+"
                            }
                        ]
                    }
                },
                "when": {
                    "activate": "0001-01-01T00:00:00.0000000+00:00",
                    "deactivate": "9999-12-31T23:59:59.9999999+00:00"
                },
                "permission": "Update",
                "links": {"self": "some-link"},
                "limit": {
                    "scope": "LUSID_SYSTEM",
                    "code": "lusid-administrator"
                }
            }
        ]

    def test_create_with_new_resource_request(self, respx_mock, stock_policy_resource):
        respx_mock.post("/access/api/roles").mock(return_value=httpx.Response(200, json={}))
        # given no role exists yet and we create one using the updadted RoleResourceRequest
        sut = access.RoleResource(
            id="role1",
            code="a_role_code",
            resource=access.RoleResourceRequest(
                policy_id_role_resource=access.PolicyIdRoleResource(policies=[stock_policy_resource])
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        sut.create(self.client)
        # then a post request is made
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/roles"
        assert json.loads(request.content) == {
            "code": "a_role_code",
            "permission": "Read",
            "resource": {
                "policyIdRoleResource": {
                    "policies": [{"code": "a_policy", "scope": "default"}],
                    "policyCollections": [],
                }
            },
            "when": {"activate": "2024-03-01:00:00.0000000+00:00"},
        }

    def test_can_parse_role_with_policy_id(self):
        serialized = {
            "id": "whatever",
            "code": "a_role_code",
            "permission": "Read",
            "resource": {
                "policyIdRoleResource": {
                    "policies": [],
                    "policyCollections": [],
                }
            },
            "when": {"activate": "2024-03-01:00:00.0000000+00:00"},
        }
        sut = access.RoleResource.model_validate(serialized, context={"$refs": {}})
        assert sut.resource
        assert sut.resource.policy_id_role_resource is not None

    def test_can_parse_role_with_non_transitive(self):
        serialized = {
            "id": "whatever",
            "code": "a_role_code",
            "permission": "Read",
            "resource": {
                "nonTransitiveSupervisorRoleResource": {
                    "roles": [],
                }
            },
            "when": {"activate": "2024-03-01:00:00.0000000+00:00"},
        }
        sut = access.RoleResource.model_validate(serialized, context={"$refs": {}})
        assert sut.resource
        assert sut.resource.non_transitive_supervisor_role_resource is not None

    def test_create_with_nontransitive(self, respx_mock, stock_policy_resource):
        respx_mock.post("/access/api/roles").mock(return_value=httpx.Response(200, json={}))
        # given a pre-existing supervisor role
        sup = access.RoleResource(
            id="sup",
            code="supervisor",
            resource=access.RoleResourceRequest(
                policy_id_role_resource=access.PolicyIdRoleResource(policies=[])
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        # when we create a new role which references the supervisor
        sut = access.RoleResource(
            id="role1",
            code="a_role_code",
            resource=access.RoleResourceRequest(
                non_transitive_supervisor_role_resource=access.NonTransitiveSupervisorRoleResource(
                    roles=[sup]
                )
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        sut.create(self.client)
        # then a post request is made
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/roles"
        assert json.loads(request.content) == {
            "code": "a_role_code",
            "permission": "Read",
            "resource": {
                "nonTransitiveSupervisorRoleResource": {
                    "roles": [{"scope": "default", "code": "supervisor"}]
                }
            },
            "when": {"activate": "2024-03-01:00:00.0000000+00:00"},
        }

    def test_create_with_policy_collection(self, respx_mock, stock_policy_resource):
        respx_mock.post("/access/api/roles").mock(return_value=httpx.Response(200, json={}))
        respx_mock.post("/access/api/policycollections").mock(
            return_value=httpx.Response(200, json={"id": {"scope": "default", "code": "col-inner"}})
        )
        respx_mock.post("/access/api/policycollections").mock(
            return_value=httpx.Response(200, json={"id": {"scope": "default", "code": "col-code"}})
        )
        # given an inner collection which has already been created
        inner = access.PolicyCollectionResource(id="polly-collection", code="col-code", policies=[])
        inner.create(self.client)
        # when we create a role referenccing the collection
        sut = access.RoleResource(
            id="role1",
            code="a_role_code",
            resource=access.RoleResourceRequest(
                policy_id_role_resource=access.PolicyIdRoleResource(policy_collections=[inner])
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        sut.create(self.client)
        # then a post request is made
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/roles"
        assert json.loads(request.content) == {
            "code": "a_role_code",
            "permission": "Read",
            "resource": {
                "policyIdRoleResource": {
                    "policies": [],
                    "policyCollections": [{"scope": "default", "code": "col-code"}],
                }
            },
            "when": {"activate": "2024-03-01:00:00.0000000+00:00"},
        }

    def test_delete(self, respx_mock, stock_policy_resource):
        respx_mock.delete("/access/api/roles/a_role").mock(side_effect=[httpx.Response(200, json={})])
        # given an existing policy
        # when we delete it
        old_state = SimpleNamespace(id="role1", code="a_role")
        access.RoleResource.delete(self.client, old_state)
        # then a delete request is made
        request = respx_mock.calls.last.request
        assert request.method == "DELETE"
        assert request.url.path == "/access/api/roles/a_role"

    @staticmethod
    def test_deps(stock_policy_resource):
        # given a role with two policy resources
        sut = access.RoleResource(
            id="role1",
            code="a_role_code",
            resource=access.RoleResourceRequest(
                policy_id_role_resource=access.PolicyIdRoleResource(policies=[stock_policy_resource])
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        # when we ask for the deps we get two back
        assert sut.deps() == [stock_policy_resource]

    def test_deps_policy_collection(self, stock_policy_resource):
        # given a role with a policy collection
        inner = access.PolicyCollectionResource(
            id="polly-collection", code="col-inner", policies=[stock_policy_resource]
        )
        sut = access.RoleResource(
            id="role1",
            code="a_role_code",
            resource=access.RoleResourceRequest(
                policy_id_role_resource=access.PolicyIdRoleResource(policy_collections=[inner])
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        # when we ask for the deps
        assert sut.deps() == [inner]

    @pytest.fixture
    def read_role_response(self):
        return {
            "id": {"scope": "default", "code": "a_role_code"},
            "when": {
                "activate": "2024-03-01:00:00.0000000+00:00",
                "deactivate": "2024-03-01:00:00.0000000+00:00",
            },
            "roleHierarchyIndex": 20,
            "permission": "Read",
            "resource": {
                "policyIdRoleResource": {"policies": [{"code": "a_policy", "scope": "default"}]}
            },
            "links": [],
        }

    def test_update_remove_policies(self, respx_mock, read_role_response):
        # given the role exists and has a policy
        respx_mock.get("/access/api/roles/a_role_code").mock(
            side_effect=[httpx.Response(200, json=read_role_response)]
        )
        respx_mock.put("/access/api/roles/a_role_code").mock(side_effect=[httpx.Response(200, json={})])
        # and the desired state is no policies
        sut = access.RoleResource(
            id="role1",
            code="a_role_code",
            resource=access.RoleResourceRequest(
                policy_id_role_resource=access.PolicyIdRoleResource(policies=[])
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        # when we update
        log = sut.update(self.client, SimpleNamespace(id="role1", code="a_role_code"))
        # then the log returned is the same
        assert log == {"code": "a_role_code", "id": "role1"}
        # and a put request to modify the role was made
        request = respx_mock.calls.last.request
        assert request.method == "PUT"
        assert request.url.path == "/access/api/roles/a_role_code"
        assert json.loads(request.content) == {
            "permission": "Read",  # nb no code set when its a PUT
            "resource": {"policyIdRoleResource": {"policies": [], "policyCollections": []}},
            "when": {"activate": "2024-03-01:00:00.0000000+00:00"},
        }

    def test_update_non_transitive(self, respx_mock, read_role_response):
        # given the role exists and has a policy
        respx_mock.get("/access/api/roles/a_role_code").mock(
            side_effect=[httpx.Response(200, json=read_role_response)]
        )
        respx_mock.put("/access/api/roles/a_role_code").mock(side_effect=[httpx.Response(200, json={})])
        # and there is an existing supervisor role
        sup = access.RoleResource(
            id="sup",
            code="supervisor",
            resource=access.RoleResourceRequest(
                policy_id_role_resource=access.PolicyIdRoleResource(policies=[])
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        # and the desired state only a nonTransitiveSupervisor....
        sut = access.RoleResource(
            id="role1",
            code="a_role_code",
            resource=access.RoleResourceRequest(
                non_transitive_supervisor_role_resource=access.NonTransitiveSupervisorRoleResource(
                    roles=[sup]
                )
            ),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        # when we update
        log = sut.update(self.client, SimpleNamespace(id="role1", code="a_role_code"))
        # then the log returned is the same
        assert log == {"code": "a_role_code", "id": "role1"}
        # and a put request to modify the role was made
        request = respx_mock.calls.last.request
        assert request.method == "PUT"
        assert request.url.path == "/access/api/roles/a_role_code"
        assert json.loads(request.content) == {
            "permission": "Read",  # nb no code set when its a PUT
            "resource": {
                "nonTransitiveSupervisorRoleResource": {
                    "roles": [{"scope": "default", "code": "supervisor"}]
                }
            },
            "when": {"activate": "2024-03-01:00:00.0000000+00:00"},
        }

    def test_update_non_transitive_ref(self, respx_mock, read_role_response):
        # given the role exists and has a policy
        respx_mock.get("/access/api/roles/a_role_code").mock(
            side_effect=[httpx.Response(200, json=read_role_response)]
        )
        respx_mock.put("/access/api/roles/a_role_code").mock(side_effect=[httpx.Response(200, json={})])
        # and we reference a system role
        ref = access.RoleRef(id="supervisor", scope="support", code="support-iam-readonly")
        # and the desired state only a nonTransitiveSupervisor....
        ntsrs = access.NonTransitiveSupervisorRoleResource(roles=[ref])
        sut = access.RoleResource(
            id="role1",
            code="a_role_code",
            resource=access.RoleResourceRequest(non_transitive_supervisor_role_resource=ntsrs),
            when=access.WhenSpec(activate="2024-03-01:00:00.0000000+00:00"),
            permission=access.Permission.READ,
        )
        # when we update
        log = sut.update(self.client, SimpleNamespace(id="role1", code="a_role_code"))
        # then the log returned is the same
        assert log == {"code": "a_role_code", "id": "role1"}
        # and a put request to modify the role was made
        request = respx_mock.calls.last.request
        assert request.method == "PUT"
        assert request.url.path == "/access/api/roles/a_role_code"
        assert json.loads(request.content) == {
            "permission": "Read",  # nb no code set when its a PUT
            "resource": {
                "nonTransitiveSupervisorRoleResource": {
                    "roles": [{"scope": "support", "code": "support-iam-readonly"}]
                }
            },
            "when": {"activate": "2024-03-01:00:00.0000000+00:00"},
        }

    def test_dump(self, stock_policy_resource):
        # given a role resource with policies
        sut = access.RoleResource(
            id="role1",
            code="test-role",
            description="A test role with policies",
            resource=access.RoleResourceRequest(
                policy_id_role_resource=access.PolicyIdRoleResource(
                    policies=[stock_policy_resource]
                )
            ),
            when=access.WhenSpec(activate="2024-01-01T00:00:00.0000000+00:00"),
            permission=access.Permission.READ
        )
        # when we dump it
        dumped = sut.model_dump(
            mode="json",
            by_alias=True,
            round_trip=True,
            exclude_none=True,
            context={"style": "dump"}
        )
        # then the dumped state is correct
        assert dumped == {
            "code": "test-role",
            "description": "A test role with policies",
            "resource": {
                "policyIdRoleResource": {
                    "policies": [
                        {"$ref": "policy1"}
                    ],
                    "policyCollections": []
                }
            },
            "when": {
                "activate": "2024-01-01T00:00:00.0000000+00:00"
            },
            "permission": "Read"
        }

    def test_undump(self, stock_policy_resource):
        # given a dumped role state
        dumped = {
            "code": "test-role",
            "description": "A test role with policies",
            "resource": {
                "policyIdRoleResource": {
                    "policies": [
                        {"$ref": "policy1"}
                    ],
                    "policyCollections": []
                }
            },
            "when": {
                "activate": "2024-01-01T00:00:00.0000000+00:00"
            },
            "permission": "Read"
        }
        # when we undump it
        sut = access.RoleResource.model_validate(
            dumped,
            context={
                "style": "undump",
                "$refs": {
                    "policy1": stock_policy_resource,
                },
                "id": "role1",
            }
        )
        # then the id has been extracted from the context
        assert sut.id == "role1"
        assert sut.code == "test-role"
        assert sut.description == "A test role with policies"
        assert sut.permission == access.Permission.READ
        assert sut.when.activate == "2024-01-01T00:00:00.0000000+00:00"
        # and the policy references have been wired up
        assert sut.resource
        assert sut.resource.policy_id_role_resource is not None
        assert sut.resource.policy_id_role_resource.policies is not None
        assert len(sut.resource.policy_id_role_resource.policies) == 1
        assert sut.resource.policy_id_role_resource.policies[0] == stock_policy_resource
        assert sut.resource.policy_id_role_resource.policy_collections is not None
        assert len(sut.resource.policy_id_role_resource.policy_collections) == 0

    def test_from_api(self, role_resource):
        role_data = access.RoleResource.from_api(role_resource[0])
        resource = access.RoleResource.model_validate(role_data)
        assert resource.id == "role_default_testid68487f04a1f047fb864187e6594abd4f_-+"
        assert resource.scope == "default"
        assert resource.code == "testid68487f04a1f047fb864187e6594abd4f_-+"

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_list(self, respx_mock, role_resource):
        # GIVEN expected result structure
        expected_result = {
            "values": [{
                "value": {
                    "code": "testid68487f04a1f047fb864187e6594abd4f_-+",
                    "description": "TestRole",
                    "id": "role_default_testid68487f04a1f047fb864187e6594abd4f_-+",
                    "permission": "Update",
                    "resource": {
                        "policyIdRoleResource": {
                            "policies": [
                                {
                                    "code": "testid68487f04a1f047fb864187e6594abd4f_-+",
                                    "id": "policy_lusid_system_testid68487f04a1f047fb864187e6594abd4f"
                                          "_-+",
                                    "scope": "LUSID_SYSTEM",
                                },
                            ],
                            "policyCollections": [
                                {
                                    "code":
                                        "testid68487f04a1f047fb864187e6594abd4f_-+",
                                    "id":
                                        "policy-collection_lusid_system_testid68487f04a1f047fb864187e6594abd4f_-+",
                                    "scope": "LUSID_SYSTEM",
                                }]

                        },
                    },
                    "roleHierarchyIndex": 2216,
                    "scope": "default",
                    "when": {
                        "activate": "0001-01-01T00:00:00.0000000+00:00",
                        "deactivate": "9999-12-31T23:59:59.9999999+00:00",
                    },
                },
                "keys": {
                    "code": "testid68487f04a1f047fb864187e6594abd4f_-+"
                }
            }],
        }
        # AND a mocked list endpoint
        mock_request(respx_mock, access.RoleResource.list_endpoint(), role_resource)
        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        # WHEN calling the list method
        result = await access.RoleResource.list(
            async_client,
            query_params={}
        )
        # THEN the result excludes internal fields
        assert "links" not in result
        assert "href" not in result
        # AND contains the expected values
        # AND the resource can be validated
        resource = access.RoleResource.model_validate(result["values"][0]["value"])
        assert resource.id == "role_default_testid68487f04a1f047fb864187e6594abd4f_-+"
        # AND the correct request was made
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == "/access/api/roles"
        assert request.url.query == b""
        # AND matches expected result
        assert result == expected_result

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_fetch(self, respx_mock, role_resource):
        # GIVEN a Role resource with specific scope and code
        resource = role_resource[0]
        scope = resource["id"]["scope"]
        code = resource["id"]["code"]
        # AND the resource exists in LUSID
        respx_mock.get(
            f"/access/api/roles/{code}"
        ).mock(
            return_value=httpx.Response(200, json=resource)
        )
        # WHEN the fetch method is called for that resource
        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        response = await access.RoleResource.fetch(
            async_client,
            keys={"scope": scope, "code": code}
        )
        # THEN the response is as expected
        pir_code = resource["resource"]["policyIdRoleResource"]["policies"][0]["code"]
        pir_scope = resource["resource"]["policyIdRoleResource"]["policies"][0]["scope"]
        pc_code = resource["resource"]["policyIdRoleResource"]["policyCollections"][0]["code"]
        pc_scope = resource["resource"]["policyIdRoleResource"]["policyCollections"][0]["scope"]
        assert response == {
            "id": f"role_{scope}_{code}",
            "scope": scope,
            "code": code,
            "roleHierarchyIndex": resource["roleHierarchyIndex"],
            "description": resource["description"],
            "resource": {
                "policyIdRoleResource": {
                    "policies": [
                        {
                            "id": f"policy_{pir_scope}_{pir_code}".lower(),
                            "scope": pir_scope,
                            "code": pir_code
                        }
                    ],
                    "policyCollections": [
                        {
                            "id": f"policy-collection_{pc_scope}_{pc_code}".lower(),
                            "scope": pc_scope,
                            "code": pc_code
                        }
                    ]
                }
            },
            "when": resource["when"],
            "permission": resource["permission"],
        }
        # AND the correct request was made to LUSID
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == f"/access/api/roles/{code}"
        assert request.url.query == b"scope=default"


@pytest.mark.respx(base_url=TEST_BASE)
class DescribePolicyCollectionRef:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [response_hook]})

    def test_attach_when_present(self, respx_mock):
        # given that the remote definition exists
        respx_mock.get("/access/api/policycollections/cd1?scope=sc1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        sut = access.PolicyCollectionRef(id="one", scope="sc1", code="cd1")
        # when we call attach
        sut.attach(client)
        # then a get request was made and no exception raised

    def test_attach_when_missing(self, respx_mock):
        # given the remote does not exist
        respx_mock.get("/access/api/policycollections/cd1?scope=sc1").mock(
            return_value=httpx.Response(404, json={})
        )
        client = self.client
        sut = access.PolicyCollectionRef(id="one", scope="sc1", code="cd1")
        # when we call attach an exception is raised
        with pytest.raises(RuntimeError) as ex:
            sut.attach(client)
        assert "PolicyCollection sc1/cd1 not found" in str(ex.value)

    def test_attach_when_http_error(self, respx_mock):
        # given the server returns an error
        respx_mock.get("/access/api/policycollections/cd1?scope=sc1").mock(
            return_value=httpx.Response(500, json={})
        )
        client = self.client
        sut = access.PolicyCollectionRef(id="one", scope="sc1", code="cd1")
        # when we call attach an exception is raised
        with pytest.raises(httpx.HTTPError):
            sut.attach(client)


@pytest.mark.respx(base_url=TEST_BASE)
class DescribePolicyCollectionResource:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [response_hook]})

    @pytest.fixture
    def policy1(self):
        return access.PolicyResource(
            id="policy1",
            code="a_policy1",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="banjo",
                    description="whatever",
                    identifier={"scope": "w", "code": "y"},
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )

    @pytest.fixture
    def policy2(self):
        return access.PolicyResource(
            id="policy2",
            code="a_policy2",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="banjo",
                    description="whatever",
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )

    @pytest.fixture
    def policy_collection_resource(self):
        return {
            "links": ["test-link"],
            "href": "test-href",
            "values": [
                {
                    "id": {
                        "scope": "default",
                        "code": "00002c43-fe62-4cc6-80aa-e36fadcfe225"
                    },
                    "policies": [
                        {
                            "scope": "default",
                            "code": "00002c43-fe62-4cc6-80aa-e36fadcfe225"
                        }
                    ],
                    "description": "examplePolicyCollection"
                }
            ],
            "nextPage": "CgAsAGRlTE4M2FcAAAAAAAAAAAAAAAAAAAAA"
        }

    def test_create_with_policies(self, respx_mock, policy1, policy2):
        respx_mock.post("/access/api/policycollections").mock(
            return_value=httpx.Response(200, json={"id": {"scope": "default", "code": "col-code"}})
        )
        client = self.client
        # given a desired collection
        sut = access.PolicyCollectionResource(
            id="polly-collection", code="col-code", policies=[policy1, policy2]
        )
        # when we create it
        state = sut.create(client)
        # then the state is returned
        assert state == {"scope": "default", "code": "col-code"}
        # and a create request was sent
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/policycollections"
        assert json.loads(request.content) == {
            "code": "col-code",
            "policies": [
                {"scope": "default", "code": "a_policy1"},
                {"scope": "default", "code": "a_policy2"},
            ],
            "policyCollections": [],
        }

    def test_create_with_policy_ref(self, respx_mock, policy1, policy2):
        respx_mock.post("/access/api/policycollections").mock(
            return_value=httpx.Response(200, json={"id": {"scope": "default", "code": "col-code"}})
        )
        client = self.client
        # given a policy ref to a non-default scope (eg a ref to a builtin
        # policy like an admin one
        ref = access.PolicyRef(id="ref", scope="sc1", code="cd1")
        # given a desired collection
        sut = access.PolicyCollectionResource(id="polly-collection", code="col-code", policies=[ref])
        # when we create it
        state = sut.create(client)
        # then the state is returned
        assert state == {"scope": "default", "code": "col-code"}
        # and a create request was sent
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/policycollections"
        assert json.loads(request.content) == {
            "code": "col-code",
            "policies": [{"scope": "sc1", "code": "cd1"}],
            "policyCollections": [],
        }

    def test_create_with_nested_collection(self, respx_mock, policy1, policy2):
        respx_mock.post("/access/api/policycollections").mock(
            return_value=httpx.Response(200, json={"id": {"scope": "default", "code": "col-inner"}})
        )
        respx_mock.post("/access/api/policycollections").mock(
            return_value=httpx.Response(200, json={"id": {"scope": "default", "code": "col-code"}})
        )
        client = self.client
        # given an inner collection which has already been created
        inner = access.PolicyCollectionResource(
            id="polly-collection", code="col-inner", policies=[policy1, policy2]
        )
        inner.create(self.client)
        # and a desired collection which references it
        sut = access.PolicyCollectionResource(
            id="polly-collection", code="col-code", policy_collections=[inner]
        )
        # when we create it
        state = sut.create(client)
        # then the state is returned
        assert state == {"scope": "default", "code": "col-code"}
        # and a create request was sent
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/policycollections"
        assert json.loads(request.content) == {
            "code": "col-code",
            "policyCollections": [{"scope": "default", "code": "col-inner"}],
            "policies": [],
        }

    def test_update_no_change(self, respx_mock, policy1, policy2):
        # given an existing collection
        respx_mock.get("/access/api/policycollections/col-code").mock(
            return_value=httpx.Response(
                200,
                json={
                    "id": {"code": "col-code", "scope": "default"},
                    "policies": [
                        {"scope": "default", "code": "a_policy1"},
                        {"scope": "default", "code": "a_policy2"},
                    ],
                    "policyCollections": [],
                    "links": [],
                },
            )
        )
        old_state = SimpleNamespace(scope="default", code="col-code")
        # given a desired collection
        sut = access.PolicyCollectionResource(
            id="polly-collection", code="col-code", policies=[policy1, policy2]
        )
        # when we update
        state = sut.update(self.client, old_state)
        # then state is None and no further requests are made
        assert state is None

    def test_update_rename(self, respx_mock, policy1, policy2):
        respx_mock.delete("/access/api/policycollections/col-code").mock(
            side_effect=[httpx.Response(200, json={})]
        )
        respx_mock.post("/access/api/policycollections").mock(
            return_value=httpx.Response(200, json={"id": {"scope": "default", "code": "col-code2"}})
        )
        # given an existing collection at col-code
        old_state = SimpleNamespace(scope="default", code="col-code")
        # and a desired collection with code col-code2
        sut = access.PolicyCollectionResource(
            id="polly-collection", code="col-code2", policies=[policy1, policy2]
        )
        # when we update
        state = sut.update(self.client, old_state)
        # the new state is returned
        assert state == {"scope": "default", "code": "col-code2"}
        # and a delete and a create call were made

    def test_update_remove_policy(self, respx_mock, policy1, policy2):
        # given an existing collection
        respx_mock.get("/access/api/policycollections/col-code").mock(
            return_value=httpx.Response(
                200,
                json={
                    "id": {"code": "col-code", "scope": "default"},
                    "policies": [
                        {"scope": "default", "code": "a_policy1"},
                        {"scope": "default", "code": "a_policy2"},
                    ],
                    "policyCollections": [],
                    "links": [],
                },
            )
        )
        respx_mock.put("/access/api/policycollections/col-code").mock(
            return_value=httpx.Response(200, json={"id": {"scope": "default", "code": "col-code"}})
        )
        old_state = SimpleNamespace(scope="default", code="col-code")
        # given a desired collection
        sut = access.PolicyCollectionResource(id="polly-collection", code="col-code", policies=[policy1])
        # when we update
        state = sut.update(self.client, old_state)
        # then state returned
        assert state == {"scope": "default", "code": "col-code"}
        # and a put request was sent
        request = respx_mock.calls.last.request
        assert request.method == "PUT"
        assert request.url.path == "/access/api/policycollections/col-code"
        assert json.loads(request.content) == {
            "policies": [{"scope": "default", "code": "a_policy1"}],
            "policyCollections": [],
        }

    def test_delete(self, respx_mock):
        respx_mock.delete("/access/api/policycollections/col-code").mock(
            side_effect=[httpx.Response(200, json={})]
        )
        # given an existing collection
        # when we delete it
        old_state = SimpleNamespace(scope="default", code="col-code")
        access.PolicyCollectionResource.delete(self.client, old_state)
        # then a delete request is made
        request = respx_mock.calls.last.request
        assert request.method == "DELETE"
        assert request.url.path == "/access/api/policycollections/col-code"

    def test_deps_policy_ref(self, respx_mock, policy1, policy2):
        ref = access.PolicyRef(id="ref", scope="sc1", code="cd1")
        # given a desired collection with two policies
        sut = access.PolicyCollectionResource(id="polly-collection", code="col-code", policies=[ref])
        # when we get the deps
        deps = sut.deps()
        assert deps == [ref]

    def test_deps_policies_only(self, respx_mock, policy1, policy2):
        # given a desired collection with two policies
        sut = access.PolicyCollectionResource(
            id="polly-collection", code="col-code", policies=[policy1, policy2]
        )
        # when we get the deps
        deps = sut.deps()
        assert deps == [policy1, policy2]

    def test_deps_collections_only(self, respx_mock, policy1, policy2):
        # given an inner collection which has already been created
        inner = access.PolicyCollectionResource(
            id="polly-collection", code="col-inner", policies=[policy1, policy2]
        )
        # and a desired collection which references it
        sut = access.PolicyCollectionResource(
            id="polly-collection", code="col-code", policy_collections=[inner]
        )
        # when we get the deps
        deps = sut.deps()
        assert deps == [inner]

    def test_dump(self, policy1, policy2):
        # given an existing policy collection
        sut = access.PolicyCollectionResource(
            id="collection1",
            code="test-collection",
            description="A test collection",
            policies=[policy1, policy2],
            metadata={"key1": ["value1", "value2"], "key2": ["value3"]}
        )
        # when we dump it
        dumped = sut.model_dump(
            by_alias=True,
            round_trip=True,  # excludes computeds
            exclude_none=True,
            context={"style": "dump"}
        )
        # then the dumped state is correct
        assert dumped == {
            "code": "test-collection",
            "description": "A test collection",
            "policies": [
                {"$ref": policy1.id},
                {"$ref": policy2.id}
            ],
            "policyCollections": [],
            "metadata": {
                "key1": ["value1", "value2"],
                "key2": ["value3"]
            }
        }

    def test_undump(self, policy1, policy2):
        # given a dumped policy collection state
        dumped = {
            "code": "test-collection",
            "description": "A test collection",
            "policies": [
                {"$ref": policy1.id},
                {"$ref": policy2.id}
            ],
            "policyCollections": [],
            "metadata": {
                "key1": ["value1", "value2"],
                "key2": ["value3"]
            }
        }
        # when we undump it
        sut = access.PolicyCollectionResource.model_validate(
            dumped,
            context={
                "style": "undump",
                "$refs": {
                    policy1.id: policy1,
                    policy2.id: policy2,
                },
                "id": "collection1",
            }
        )
        # then the id has been extracted from the context
        assert sut.id == "collection1"
        assert sut.code == "test-collection"
        assert sut.description == "A test collection"
        # and the policy refs have been wired up
        assert sut.policies == [policy1, policy2]
        assert sut.policy_collections == []
        assert sut.metadata == {
            "key1": ["value1", "value2"],
            "key2": ["value3"]
        }

    def test_parse_api_format(self):
        policy1 = access.PolicyRef(
            id="p1",
            scope="sc1",
            code="cd1",
        )
        coll1 = access.PolicyCollectionRef(
            id="c1",
            scope="sc1",
            code="cd1"
        )
        api_format = {
            "id": {
                "scope": "default",
                "code": "organisation-wide-policies"
            },
            "policies": [
                {"$ref": "p1"}
            ],
            "policyCollections": [
                {"$ref": "c1"}
            ],
            "description": "Collection of organisation wide policies"
        }
        parsed = access.PolicyCollectionResource.model_validate(
            api_format,
            context={"id": "someid", "$refs": {
                "p1": policy1,
                "c1": coll1
            }}
        )
        assert parsed.id == "someid"
        assert parsed.policies == [policy1]
        assert parsed.policy_collections == [coll1]

    def test_from_api(self, policy_collection_resource):
        policy_coll_data = access.PolicyCollectionResource.from_api(
            policy_collection_resource["values"][0]
        )
        resource = access.PolicyCollectionResource.model_validate(policy_coll_data)
        assert resource.id == "policy-collection_default_00002c43-fe62-4cc6-80aa-e36fadcfe225"
        assert resource.scope == "default"
        assert resource.code == "00002c43-fe62-4cc6-80aa-e36fadcfe225"

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_list(self, respx_mock, policy_collection_resource):
        # GIVEN expected result structure
        expected_result = {
            "values": [
                {
                    "value": {
                        "id": "policy-collection_default_00002c43-fe62-4cc6-80aa-e36fadcfe225",
                        "policies": [
                            {
                                "id": "policy_default_00002c43-fe62-4cc6-80aa-e36fadcfe225",
                                "scope": "default",
                                "code": "00002c43-fe62-4cc6-80aa-e36fadcfe225"
                            }],
                        "description": "examplePolicyCollection",
                        "scope": "default",
                        "code": "00002c43-fe62-4cc6-80aa-e36fadcfe225",
                        "metadata": {}
                    },
                    "keys": {
                        "scope": "default",
                        "code": "00002c43-fe62-4cc6-80aa-e36fadcfe225"
                    }}],
            "nextPage": "CgAsAGRlTE4M2FcAAAAAAAAAAAAAAAAAAAAA"}
        # AND a mocked list endpoint
        mock_request(
            respx_mock,
            access.PolicyCollectionResource.list_endpoint(),
            policy_collection_resource
        )
        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        # WHEN calling the list method with query params
        result = await access.PolicyCollectionResource.list(
            async_client,
            query_params={
                "limit": 50,
                "filter": "test",
                "page": "next_page"
            }
        )
        # THEN the result matches expected structure
        assert result == expected_result
        resource = access.PolicyCollectionResource.model_validate(result["values"][0]["value"])
        assert resource.id == "policy-collection_default_00002c43-fe62-4cc6-80aa-e36fadcfe225"
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == "/access/api/policycollections/page"
        assert request.url.query == b"limit=50&filter=test&page=next_page"

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_fetch(self, respx_mock):
        # GIVEN a PolicyCollection resource with specific scope and code
        scope = "default"
        code = "code1"
        # AND the resource exists in LUSID
        respx_mock.get(
            f"/access/api/policycollections/{code}"
        ).mock(
            return_value=httpx.Response(200, json={
                "id": {
                    "scope": scope,
                    "code": code
                },
                "displayName": "display_name",
                "scope": scope,
                "code": code,
                "description": "some_description",
                "applications": [
                    "LUSID"
                ],
                "tags": [
                    "example_tag"
                ],
                "templatedSelectors": [
                    {
                        "application": "LUSID",
                        "tag": "example_tag",
                        "selector": {
                            "idSelectorDefinition": {
                                "identifier": {
                                    "scope": "w"
                                },
                                "actions": [
                                    {
                                        "scope": "default",
                                        "activity": "Read",
                                        "entity": "Portfolio"
                                    },
                                ],
                                "name": "example_id_selector",
                                "description": "test_description"
                            }
                        }
                    }
                ],
                "links": {"self": "some-link"},
            })
        )
        # WHEN the fetch method is called for that resource
        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        response = await access.PolicyCollectionResource.fetch(
            async_client,
            keys={"scope": scope, "code": code}
        )
        # THEN the response is as expected
        assert response == {
            "id": f"policy-collection_{scope}_{code}",
            "displayName": "display_name",
            "scope": scope,
            "code": code,
            "description": "some_description",
            "applications": [
                "LUSID"
            ],
            "tags": [
                "example_tag"
            ],
            "metadata": {},
            "templatedSelectors": [
                {
                    "application": "LUSID",
                    "tag": "example_tag",
                    "selector": {
                        "idSelectorDefinition": {
                            "identifier": {
                                "scope": "w"
                            },
                            "actions": [
                                {
                                    "scope": "default",
                                    "activity": "Read",
                                    "entity": "Portfolio"
                                },
                            ],
                            "name": "example_id_selector",
                            "description": "test_description"
                        }
                    }
                }
            ],
        }
        # AND the correct request was made to LUSID
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == "/access/api/policycollections/code1"
        assert request.url.query == b"scope=default"


@pytest.mark.respx(base_url=TEST_BASE)
class DescribePolicyTemplateRef:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [response_hook]})

    @staticmethod
    def test_scope_default():
        # when scope is omitted
        sut = access.PolicyTemplateRef(id="one", code="cd")
        # then it uses "default"
        assert sut.scope == "default"

    def test_attach_when_present(self, respx_mock):
        # given that the remote definition exists
        respx_mock.get("/access/api/policytemplates/pol1?scope=sc1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        sut = access.PolicyTemplateRef(id="one", scope="sc1", code="pol1")

        # when we call attach
        sut.attach(client)
        # then a get request was made and no exception raised

    def test_attach_when_missing(self, respx_mock):
        # given the remote does not exist
        respx_mock.get("/access/api/policytemplates/pol1?scope=sc1").mock(
            return_value=httpx.Response(404, json={})
        )
        client = self.client
        sut = access.PolicyTemplateRef(id="one", scope="sc1", code="pol1")
        # when we call attach an exception is raised
        with pytest.raises(RuntimeError) as ex:
            sut.attach(client)
        assert "Policy Template sc1/pol1 not found" in str(ex.value)

    def test_attach_when_http_error(self, respx_mock):
        # given the server returns an error
        respx_mock.get("/access/api/policytemplates/pol1?scope=sc1").mock(
            return_value=httpx.Response(500, json={})
        )
        client = self.client
        sut = access.PolicyTemplateRef(id="one", scope="sc1", code="pol1")
        # when we call attach an exception is raised
        with pytest.raises(httpx.HTTPError):
            sut.attach(client)


@pytest.mark.respx(base_url=TEST_BASE)
class DescribePolicyTemplate:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [response_hook]})

    @pytest.fixture
    def policy1(self):
        return access.PolicyResource(
            id="policy1",
            code="a_policy1",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.IdSelector(
                    name="banjo",
                    description="whatever",
                    identifier={"scope": "w", "code": "y"},
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )

    @pytest.fixture
    def policy2(self):
        return access.PolicyResource(
            id="policy2",
            code="a_policy2",
            description="policy description",
            applications=["lusid"],
            grant=access.Grant.ALLOW,
            when=access.WhenSpec(activate="2024-08-31T18:00:00.0000000+00:00"),
            selectors=[
                access.MatchAllSelector(
                    name="banjo",
                    description="whatever",
                    actions=[access.ActionId(scope="actscope", activity="execute", entity="Feature")],
                )
            ],
        )

    @pytest.fixture
    def stock_policy_template_resource(self):
        selector = access.IdSelector(
            name="example_id_selector",
            description="test_description",
            identifier={"scope": "w"},
            actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
        )
        return access.PolicyTemplateResource(
            id="policy_template1",
            display_name="display_name",
            code="code1",
            description="some_description",
            templated_selectors=[access.TemplatedSelector(
                application="LUSID",
                tag="Data",
                selector=selector
            )]
        )

    @pytest.fixture
    def policy_template_resource(self):
        return {
            "links": ["test-link"],
            "href": "test-href",
            "values": [
                {
                    "displayName": "updated-policy-template",
                    "scope": "default",
                    "code": "mvb1",
                    "description": "Example policy template for "
                                   "a policy that grants access to some resource",
                    "applications": [
                        "LUSID"
                    ],
                    "tags": [
                        "Data"
                    ],
                    "templatedSelectors": [
                        {
                            "application": "LUSID",
                            "tag": "Data",
                            "selector": {
                                "idSelectorDefinition": {
                                    "identifier": {
                                        "scope": "official1"
                                    },
                                    "actions": [
                                        {
                                            "scope": "default",
                                            "activity": "Read",
                                            "entity": "Portfolio"
                                        },
                                        {
                                            "scope": "default",
                                            "activity": "Aggregate",
                                            "entity": "Portfolio"
                                        }
                                    ],
                                    "name": "access-official-scope",
                                    "description": "Allow readonly access to the 'official' scope"
                                }
                            }
                        },
                        {
                            "application": "LUSID",
                            "tag": "Data",
                            "selector": {
                                "idSelectorDefinition": {
                                    "identifier": {
                                        "scope": "official12"
                                    },
                                    "actions": [
                                        {
                                            "scope": "default",
                                            "activity": "Read",
                                            "entity": "Portfolio"
                                        },
                                        {
                                            "scope": "default",
                                            "activity": "Aggregate",
                                            "entity": "Portfolio"
                                        }
                                    ],
                                    "name": "access-official-scope",
                                    "description": "Allow readonly access to the 'official' scope"
                                }
                            }
                        }
                    ]
                }
            ],
            "nextPage": "test-next-page"
        }

    def test_read(self, respx_mock, stock_policy_template_resource):
        respx_mock.get("/access/api/policytemplates/code1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "displayName": "display_name",
                    "scope": "scope1",
                    "code": "code1",
                    "description": "some_description",
                    "applications": [
                        "LUSID"
                    ],
                    "tags": [
                        "example_tag"
                    ],
                    "templatedSelectors": [
                        {
                            "application": "LUSID",
                            "tag": "example_tag",
                            "selector": {
                                "idSelectorDefinition": {
                                    "identifier": {
                                        "scope": "w"
                                    },
                                    "actions": [
                                        {
                                            "scope": "default",
                                            "activity": "Read",
                                            "entity": "Portfolio"
                                        },
                                    ],
                                    "name": "example_id_selector",
                                    "description": "test_description"
                                }
                            }
                        }
                    ]
                }
            )
        )
        sut = stock_policy_template_resource
        old_state = SimpleNamespace(code=stock_policy_template_resource.code)
        result = sut.read(self.client, old_state)
        assert result is not None
        assert result["code"] == "code1"
        assert result["scope"] == "scope1"
        assert result["displayName"] == "display_name"

    def test_create(self, respx_mock, stock_policy_template_resource):
        respx_mock.post("/access/api/policytemplates").mock(return_value=httpx.Response(200, json={}))
        sut = stock_policy_template_resource
        result = sut.create(self.client)
        assert result is not None
        assert result["id"] == "policy_template1"
        assert result["code"] == "code1"
        request = respx_mock.calls.last.request
        assert request.method == "POST"
        assert request.url.path == "/access/api/policytemplates"
        assert json.loads(request.content) == {
            "code": "code1",
            "displayName": "display_name",
            "description": "some_description",
            "templatedSelectors": [
                {
                    "application": "LUSID",
                    "tag": "Data",
                    "selector": {
                        "idSelectorDefinition": {
                            "identifier": {
                                "scope": "w"
                            },
                            "actions": [
                                {
                                    "scope": "default",
                                    "activity": "Read",
                                    "entity": "Portfolio"
                                },
                            ],
                            "name": "example_id_selector",
                            "description": "test_description"
                        }
                    }
                }
            ]
        }
        assert "source_version" in result
        assert "remote_version" in result

    def test_update_no_change(self, respx_mock, stock_policy_template_resource):
        sut = stock_policy_template_resource
        remote_response = {
            "displayName": "display_name",
            "scope": "scope1",
            "code": "code1",
            "description": "some_description",
            "applications": [
                "LUSID"
            ],
            "tags": [
                "example_tag"
            ],
            "templatedSelectors": [
                {
                    "application": "LUSID",
                    "tag": "example_tag",
                    "selector": {
                        "idSelectorDefinition": {
                            "identifier": {
                                "scope": "w"
                            },
                            "actions": [
                                {
                                    "scope": "default",
                                    "activity": "Read",
                                    "entity": "Portfolio"
                                },
                            ],
                            "name": "example_id_selector",
                            "description": "test_description"
                        }
                    }
                }
            ]
        }
        respx_mock.get("/access/api/policytemplates/code1").mock(
            return_value=httpx.Response(200, json=remote_response)
        )
        # Calculate hashes to simulate no change scenario
        source_hash = sut.__get_content_hash__()
        # The remote hash should match what update() calculates from the read() response
        remote_hash = sha256(json.dumps(remote_response, sort_keys=True).encode()).hexdigest()
        old_state = SimpleNamespace(
            code="code1", source_version=source_hash, remote_version=remote_hash
        )
        result = sut.update(self.client, old_state)
        assert result is None

    def test_update_with_change(self, respx_mock):
        respx_mock.get("/access/api/policytemplates/code1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "displayName": "display_name",
                    "scope": "scope1",
                    "code": "code1",
                    "description": "some_description",
                    "applications": [
                        "LUSID"
                    ],
                    "tags": [
                        "example_tag"
                    ],
                    "templatedSelectors": [
                        {
                            "application": "LUSID",
                            "tag": "example_tag",
                            "selector": {
                                "idSelectorDefinition": {
                                    "identifier": {
                                        "scope": "w"
                                    },
                                    "actions": [
                                        {
                                            "scope": "default",
                                            "activity": "Read",
                                            "entity": "Portfolio"
                                        },
                                    ],
                                    "name": "example_id_selector",
                                    "description": "test_description"
                                }
                            }
                        }
                    ]
                }
            )
        )
        respx_mock.put("/access/api/policytemplates/code1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "displayName": "display_name_another",
                    "scope": "scope2",
                    "code": "code1",
                    "description": "some_description_another",
                    "applications": [
                        "LUSID"
                    ],
                    "tags": [
                        "example_tag"
                    ],
                    "templatedSelectors": [
                        {
                            "application": "LUSID",
                            "tag": "example_tag",
                            "selector": {
                                "idSelectorDefinition": {
                                    "identifier": {
                                        "scope": "w"
                                    },
                                    "actions": [
                                        {
                                            "scope": "default",
                                            "activity": "Read",
                                            "entity": "Portfolio"
                                        },
                                    ],
                                    "name": "example_id_selector",
                                    "description": "test_description"
                                }
                            }
                        }
                    ]
                }
            )
        )
        id_selector = access.IdSelector(
            name="example_id_selector",
            description="test_description",
            identifier={"scope": "w"},
            actions=[access.ActionId(scope="default", activity="Read", entity="Portfolio")],
        )
        templated_selector = access.TemplatedSelector(
            application="LUSID",
            tag="example_tag",
            selector=id_selector
        )
        sut = access.PolicyTemplateResource(
            id="policy_template1",
            display_name="display_name_another",
            code="code1",
            description="some_description_another",
            templated_selectors=[templated_selector]
        )
        old_state = SimpleNamespace(
            code="code1", remote_version="oldhash", source_version="different_hash"
        )
        result = sut.update(self.client, old_state)
        assert result is not None
        assert result["id"] == "policy_template1"
        assert result["code"] == "code1"
        assert "source_version" in result
        assert "remote_version" in result
        # Check for put request is called
        request = respx_mock.calls.last.request
        assert request.method == "PUT"
        assert request.url.path == "/access/api/policytemplates/code1"
        assert json.loads(request.content) == {
            "displayName": "display_name_another",
            "description": "some_description_another",
            "templatedSelectors": [
                {
                    "application": "LUSID",
                    "tag": "example_tag",
                    "selector": {
                        "idSelectorDefinition": {
                            "identifier": {
                                "scope": "w"
                            },
                            "actions": [
                                {
                                    "scope": "default",
                                    "activity": "Read",
                                    "entity": "Portfolio"
                                },
                            ],
                            "name": "example_id_selector",
                            "description": "test_description"
                        }
                    }
                }
            ]
        }

    def test_delete(self, respx_mock):
        respx_mock.delete("/access/api/policytemplates/code1").mock(
            return_value=httpx.Response(200, json={})
        )
        old_state = SimpleNamespace(code="code1")
        access.PolicyTemplateResource.delete(self.client, old_state)
        request = respx_mock.calls[-1].request
        assert request.method == "DELETE"
        assert request.url.path == "/access/api/policytemplates/code1"

    def test_deps(self, stock_policy_template_resource):
        sut = stock_policy_template_resource
        assert sut.deps() == []

    def test_dump(self, stock_policy_template_resource):
        sut = stock_policy_template_resource
        dumped = sut.model_dump(
            by_alias=True,
            round_trip=True,  # excludes computeds
            exclude_none=True,
            context={"style": "dump"}
        )
        # then the dumped state is correct
        assert dumped == {
            "code": "code1",
            "description": "some_description",
            "displayName": "display_name",
            "templatedSelectors": [{
                "application": "LUSID",
                "selector": {
                    "actions": [{
                        "activity": "Read",
                        "entity": "Portfolio",
                        "scope": "default",
                    }],
                    "description": "test_description",
                    "identifier": {
                        "scope": "w",
                    },
                    "name": "example_id_selector",
                    "typeName": "idSelectorDefinition",
                },
                "tag": "Data",
            }],
        }

    def test_undump(self):
        # given a dump
        data = {
            "code": "code1",
            "description": "some_description",
            "displayName": "display_name",
            "templatedSelectors": [{
                "application": "LUSID",
                "selector": {
                    "actions": [{
                        "activity": "Read",
                        "entity": "Portfolio",
                        "scope": "default",
                    }],
                    "description": "test_description",
                    "identifier": {
                        "scope": "w",
                    },
                    "name": "example_id_selector",
                    "typeName": "idSelectorDefinition",
                },
                "tag": "Data",
            }],
        }
        # when undumped
        des = access.PolicyTemplateResource.model_validate(
            data,
            context={"style": "dump", "id": "some-id-thing"})
        # the id is extracted from the context
        assert des.id == "some-id-thing"
        # the selector has discriminated to get the correct type
        assert len(des.templated_selectors) == 1
        assert des.templated_selectors[0].selector.type_name == "idSelectorDefinition"

    def test_parse_api_format(self):
        # given a response from the get api
        api = {
            "displayName": "display_name",
            "scope": "scope1",
            "code": "code1",
            "description": "some_description",
            "applications": [
                "LUSID"
            ],
            "tags": [
                "example_tag"
            ],
            "templatedSelectors": [
                {
                    "application": "LUSID",
                    "tag": "example_tag",
                    "selector": {
                        "idSelectorDefinition": {
                            "identifier": {
                                "scope": "w"
                            },
                            "actions": [
                                {
                                    "scope": "default",
                                    "activity": "Read",
                                    "entity": "Portfolio"
                                },
                            ],
                            "name": "example_id_selector",
                            "description": "test_description"
                        }
                    }
                }
            ]
        }
        # when parsed
        sut = access.PolicyTemplateResource.model_validate(
            api,
            context={"id": "someid"}
        )
        # then the id from the context is used for the resource id
        assert sut.id == "someid"
        # and the  selector dictionary has turned into an array
        # of selectors with a type_name on each
        assert sut.templated_selectors[0].tag == "example_tag"
        assert sut.templated_selectors[0].selector.type_name == "idSelectorDefinition"

    def test_from_api(self, policy_template_resource):
        # GIVEN expected result structure
        expected_result = {
            "code": "mvb1",
            "description": "Example policy template for a policy that grants access to some resource",
            "displayName": "updated-policy-template",
            "id": "policy-template_default_mvb1",
            "scope": "default",
            "templatedSelectors": [
                {
                    "application": "LUSID",
                    "selector": {
                        "idSelectorDefinition": {
                            "actions": [
                                {
                                    "activity": "Read",
                                    "entity": "Portfolio",
                                    "scope": "default",
                                },
                                {
                                    "activity": "Aggregate",
                                    "entity": "Portfolio",
                                    "scope": "default",
                                },
                            ],
                            "description": "Allow readonly access to the 'official' scope",
                            "identifier": {
                                "scope": "official1",
                            },
                            "name": "access-official-scope",
                        },
                    },
                    "tag": "Data",
                },
                {
                    "application": "LUSID",
                    "selector": {
                        "idSelectorDefinition": {
                            "actions": [
                                {
                                    "activity": "Read",
                                    "entity": "Portfolio",
                                    "scope": "default",
                                },
                                {
                                    "activity": "Aggregate",
                                    "entity": "Portfolio",
                                    "scope": "default",
                                },
                            ],
                            "description": "Allow readonly access to the 'official' scope",
                            "identifier": {
                                "scope": "official12",
                            },
                            "name": "access-official-scope",
                        },
                    },
                    "tag": "Data",
                },
            ],
        }
        # WHEN converting from API format
        policy_template_resource = access.PolicyTemplateResource.from_api(
            policy_template_resource["values"][0]
        )
        # THEN the result matches expected structure
        assert policy_template_resource == expected_result
        # AND the resource can be validated successfully
        resource = access.PolicyTemplateResource.model_validate(policy_template_resource)
        assert resource.id == "policy-template_default_mvb1"
        assert resource.scope == "default"
        assert resource.code == "mvb1"

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_list(self, respx_mock, policy_template_resource):
        # GIVEN expected result structure
        expected_result = {
            "values": [
                {"value": {
                    "displayName": "updated-policy-template",
                    "scope": "default",
                    "code": "mvb1",
                    "description": "Example policy template for a "
                                   "policy that grants access to some resource",
                    "templatedSelectors": [
                        {
                            "application": "LUSID",
                            "tag": "Data",
                            "selector": {"idSelectorDefinition": {
                                "identifier": {"scope": "official1"},
                                "actions": [{"scope": "default",
                                             "activity": "Read",
                                             "entity": "Portfolio"},
                                            {"scope": "default",
                                             "activity": "Aggregate",
                                             "entity": "Portfolio"}],
                                "name": "access-official-scope",
                                "description": "Allow readonly access to the 'official' scope"}}},
                        {"application": "LUSID", "tag": "Data",
                         "selector": {"idSelectorDefinition": {
                             "identifier": {"scope": "official12"},
                             "actions": [{"scope": "default",
                                          "activity": "Read",
                                          "entity": "Portfolio"},
                                         {"scope": "default",
                                          "activity": "Aggregate",
                                          "entity": "Portfolio"}],
                             "name": "access-official-scope",
                             "description": "Allow readonly access to the 'official' scope"}}}],
                    "id": "policy-template_default_mvb1"}, "keys": {"code": "mvb1"}}],
            "nextPage": "test-next-page"}
        # AND a mocked list endpoint
        mock_request(respx_mock, access.PolicyTemplateResource.list_endpoint(), policy_template_resource)
        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        # WHEN calling the list method with query params
        result = await access.PolicyTemplateResource.list(
            async_client,
            query_params={
                "limit": 10,
                "page": "next_page"
            }
        )
        # THEN the result matches expected structure
        assert result == expected_result
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == "/access/api/policytemplates"
        assert request.url.query == b"limit=10&page=next_page"

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_fetch(self, respx_mock, policy_template_resource):
        # GIVEN a PolicyTemplate resource with specific scope and code
        resource = policy_template_resource["values"][0]
        scope = resource["scope"]
        code = resource["code"]
        # AND the resource exists in LUSID
        respx_mock.get(
            f"/access/api/policytemplates/{code}"
        ).mock(
            return_value=httpx.Response(200, json=resource)
        )
        # WHEN the fetch method is called for that resource
        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        response = await access.PolicyTemplateResource.fetch(
            async_client,
            keys={"scope": scope, "code": code}
        )
        # THEN the response is as expected
        assert response == {
            "id": f"policy-template_{scope}_{code}",
            "displayName": resource["displayName"],
            "scope": scope,
            "code": code,
            "description": resource["description"],
            "templatedSelectors": resource["templatedSelectors"],
        }
        # AND the correct request was made to LUSID
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == f"/access/api/policytemplates/{code}"
        assert request.url.query == b"scope=default"
