import json
from types import SimpleNamespace

import httpx
import pytest

from fbnconfig import datatype, property

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


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 DescribeDefinitionRef:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [lambda x: x.raise_for_status()]})

    def test_attach_when_present(self, respx_mock):
        # given that the remote definition exists
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        sut = property.DefinitionRef(id="one", domain=property.Domain.Holding, 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("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(404, json={})
        )
        client = self.client
        sut = property.DefinitionRef(id="one", domain=property.Domain.Holding, scope="sc1", code="cd1")
        # when we call attach an exception is raised
        with pytest.raises(RuntimeError) as ex:
            sut.attach(client)
        assert "Property definition Holding/sc1/cd" in str(ex.value)

    def test_attach_when_http_error(self, respx_mock):
        # given the server returns an error
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(400, json={})
        )
        client = self.client
        sut = property.DefinitionRef(id="one", domain=property.Domain.Holding, 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 DescribeDefinitionResource:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [lambda x: x.raise_for_status()]})

    @pytest.fixture
    def property_refs(self):
        names = ["x", "y"]
        domain = property.Domain.Instrument
        return [
            property.DefinitionRef(id=name, domain=domain, scope="refs", code=name) for name in names
        ]

    @pytest.fixture
    def definition_resource(self):
        return {
            "links": ["test-link"],
            "href": "test-href",
            "values": [
                {
                    "key": "Instrument/lumi-prop-test-scope/lumi-prop-test-code-num",
                    "valueType": "Decimal",
                    "displayName": "lumi-prop-test-code-num",
                    "dataTypeId": {
                        "scope": "system",
                        "code": "number"
                    },
                    "type": "Metric",
                    "unitSchema": "NoUnits",
                    "domain": "Instrument",
                    "scope": "lumi-prop-test-scope",
                    "code": "lumi-prop-test-code-num",
                    "valueRequired": True,
                    "lifeTime": "Perpetual",
                    "constraintStyle": "Property",
                    "propertyDefinitionType": "ValueProperty",
                    "propertyDescription": "Luminesce test perpetual number property",
                    "properties": {},
                    "version": {
                        "effectiveFrom": "0001-01-01T00:00:00.0000000+00:00",
                        "asAtDate": "2026-01-21T18:56:58.1214700+00:00",
                        "asAtCreated": "2025-11-18T09:42:48.6563920+00:00",
                        "userIdCreated": "00uwo4k0t8vLqQkwY2p7",
                        "requestIdCreated": "febfab1a-693f-4653-9ca6-89ed6a4f44c3",
                        "reasonCreated": "",
                        "asAtModified": "2025-11-18T09:42:48.6563920+00:00",
                        "userIdModified": "00uwo4k0t8vLqQkwY2p7",
                        "requestIdModified": "febfab1a-693f-4653-9ca6-89ed6a4f44c3",
                        "reasonModified": "",
                        "asAtVersionNumber": 1,
                        "entityUniqueId": "368fd9f5-d1e4-4a89-8160-861fc2db72a0"
                    },
                    "stagedModifications": {
                        "countPending": 0,
                        "hrefPending": "https://test-host.com/api/api/stagedmodifications/?asAt=2026-01-21T18%3A56%3A58.1214700%2B00%3A00&filter=entityUniqueId%20eq%20%27368fd9f5-d1e4-4a89-8160-861fc2db72a0%27%20and%20status%20eq%20%27Pending%27",
                        "idsPreviewed": []
                    },
                    "isFilterable": True,
                    "collectionType": "test-collection-type",
                    "customEntityTypes": [],
                    "valueFormat": "test-format",
                    "href": "test-href",
                    "links": [],
                }
            ],
            "nextPage": "CgAgAEluc3Rydh5Z3gAAAAAAAAAAAAAAAAAAA=="
        }

    def test_create_value_prop(self, respx_mock):
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            life_time=property.LifeTime.Perpetual,
            constraint_style=property.ConstraintStyle.Property,
        )
        # when we call create
        state = sut.create(client)
        # then a post is made with the expected fields
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
            "lifeTime": "Perpetual",
            "constraintStyle": "Property",
        }
        assert state == {"scope": "sc1", "code": "cd1", "domain": "Holding", "derived": False}

    def test_create_value_prop_optional_fields(self, respx_mock):
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
        )
        # when we call create
        sut.create(client)
        # then a post is made with the expected fields
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
        }

    def test_create_derived_prop(self, respx_mock, property_refs):
        respx_mock.post("/api/api/propertydefinitions/derived").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("{a} + {b}", a=property_refs[0], b=property_refs[1]),
            is_filterable=False,
        )
        # when we call create
        state = sut.create(client)
        # then a post is made with the expected fields
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
            "derivationFormula": "Properties[Instrument/refs/x] + Properties[Instrument/refs/y]",
            "isFilterable": False,
        }
        assert state == {"scope": "sc1", "code": "cd1", "domain": "Holding", "derived": True}

    def test_create_value_prop_datatype_ref(self, respx_mock):
        # create definition
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        # get datatype
        respx_mock.get("/api/api/datatypes/system/currency").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # given a references to a currency datatype that exists
        dt = datatype.DataTypeRef(id="dt", scope="system", code="currency")
        dt.attach(client)
        # when we use the datatype in the property definition
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=dt,
            property_description="description",
            life_time=property.LifeTime.Perpetual,
            constraint_style=property.ConstraintStyle.Property,
        )
        # when we call create
        state = sut.create(client)
        # then a post is made using the scope and code of the datatype
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "system", "code": "currency"},
            "propertyDescription": "description",
            "lifeTime": "Perpetual",
            "constraintStyle": "Property",
        }
        assert state == {"scope": "sc1", "code": "cd1", "domain": "Holding", "derived": False}

    def test_create_value_prop_datatype_resource(self, respx_mock):
        # create definition
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        # create datatype
        respx_mock.post("/api/api/datatypes").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # given a datatype resource that gets created during the deploy
        dt = datatype.DataTypeResource(
            id="dt",
            scope="dtscope",
            code="dtcode",
            type_value_range=datatype.TypeValueRange.CLOSED,
            display_name="Priority Test",
            description="A test datatype for Priority",
            value_type=datatype.ValueType.STRING,
            acceptable_values=["High", "Medium", "Low"],
        )
        dt.create(client)
        # when we use the datatype in the property definition
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=dt,
            property_description="description",
            life_time=property.LifeTime.Perpetual,
            constraint_style=property.ConstraintStyle.Property,
        )
        # when we call create
        state = sut.create(client)
        # then a post is made using the scope and code of the datatype
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "dtscope", "code": "dtcode"},
            "propertyDescription": "description",
            "lifeTime": "Perpetual",
            "constraintStyle": "Property",
        }
        assert state == {"scope": "sc1", "code": "cd1", "domain": "Holding", "derived": False}

    @staticmethod
    def test_validates_no_is_filterable_on_value_prop():
        with pytest.raises(RuntimeError) as err:
            property.DefinitionResource(
                id="one",
                domain=property.Domain.Holding,
                scope="sc1",
                code="cd1",
                display_name="name",
                data_type_id=property.ResourceId(scope="ids", code="idc"),
                property_description="description",
                life_time=property.LifeTime.Perpetual,
                is_filterable=False,
            )
        assert (
                str(err.value)
                == "Cannot set 'is_filterable' field, a property must be either derived or plain"
        )

    @staticmethod
    def test_validates_no_lifetime_on_derived():
        with pytest.raises(RuntimeError):
            property.DefinitionResource(
                id="one",
                domain=property.Domain.Holding,
                scope="sc1",
                code="cd1",
                display_name="name",
                data_type_id=property.ResourceId(scope="ids", code="idc"),
                property_description="description",
                life_time=property.LifeTime.Perpetual,
                derivation_formula=property.Formula("3 + 4"),
            )

    @staticmethod
    def test_validates_no_constraint_on_derived():
        with pytest.raises(RuntimeError):
            property.DefinitionResource(
                id="one",
                domain=property.Domain.Holding,
                scope="sc1",
                code="cd1",
                display_name="name",
                data_type_id=property.ResourceId(scope="ids", code="idc"),
                property_description="description",
                constraint_style=property.ConstraintStyle.Identifier,
                derivation_formula=property.Formula("3 + 4"),
            )

    @staticmethod
    def test_validates_no_collection_on_derived():
        with pytest.raises(RuntimeError):
            property.DefinitionResource(
                id="one",
                domain=property.Domain.Holding,
                scope="sc1",
                code="cd1",
                display_name="name",
                data_type_id=property.ResourceId(scope="ids", code="idc"),
                property_description="description",
                collection_type=property.CollectionType.Set,
                derivation_formula=property.Formula("3 + 4"),
            )

    @staticmethod
    def test_value_prop_deps():
        # give a value property
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
        )
        # it has no dependencies
        assert sut.deps() == []

    @staticmethod
    def test_valueprop_datatype_deps():
        # given a datatype resource that gets created during the deploy
        dt = datatype.DataTypeResource(
            id="dt",
            scope="dtscope",
            code="dtcode",
            type_value_range=datatype.TypeValueRange.CLOSED,
            display_name="Priority Test",
            description="A test datatype for Priority",
            value_type=datatype.ValueType.STRING,
            acceptable_values=["High", "Medium", "Low"],
        )
        # and the property refers to the datatype
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=dt,
        )
        # the datatype is included in the deps of the property
        assert sut.deps() == [dt]

    @staticmethod
    def test_derived_prop_deps(property_refs):
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("{a} + {b}", a=property_refs[0], b=property_refs[1]),
        )
        assert sut.deps() == property_refs

    def test_formula_with_resource(self, respx_mock):
        respx_mock.post("/api/api/propertydefinitions/derived").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # given a value property
        used = property.DefinitionResource(
            id="two",
            domain=property.Domain.Instrument,
            scope="sc2",
            code="cd2",
            display_name="two",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
        )
        # and it is referenced in a formula on a derived prop
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("2 * {a}", a=used),
            is_filterable=True,
        )
        # and we call create
        sut.create(client)
        # then a post is made with identifier of used
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
            "derivationFormula": "2 * Properties[Instrument/sc2/cd2]",
            "isFilterable": True,
        }

    def test_parse_api_format_derived_prop(self):
        # given a datatype
        dt = datatype.DataTypeResource(
            id="dt",
            scope="ids",
            code="idc",
            type_value_range=datatype.TypeValueRange.CLOSED,
            display_name="Priority Test",
            description="A test datatype for Priority",
            value_type=datatype.ValueType.STRING,
        )
        # given a derived property in almost api format
        example = {
            "id": "one",
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"$ref": "dt_resource_id"},
            "propertyDescription": "description",
            "derivationFormula": {"formula": "{a} + {b}", "args": {"a": "x", "b": "y"}},
        }
        # when we parse it
        context = {"$refs": {"dt_resource_id": dt}}
        sut = property.DefinitionResource.model_validate(example, context=context)
        # the property fields are set
        assert sut.scope == "sc1"
        # and the datatype has been extracted from the context argsp
        assert sut.data_type_id.scope == "ids"
        assert sut.data_type_id.code == "idc"

    def test_formula_with_non_resource(self, respx_mock):
        respx_mock.post("/api/api/propertydefinitions/derived").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # given a derived formula with numbers passed in
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("2 * {a}", a=17),
        )
        # and we call create
        sut.create(client)
        # then a post is made with identifier of used
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
            "derivationFormula": "2 * 17",
        }
        # and the deps are empty
        assert sut.deps() == []

    def test_update_no_change(self, respx_mock):
        # given a derived property exists with y=2x17
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name",
                    "dataTypeId": {"scope": "ids", "code": "idc"},
                    "propertyDescription": "description",
                    "derivationFormula": "2 * 17",
                    "isFilterable": False,
                },
            )
        )
        client = self.client
        # when we update with the same properties
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("2 * {a}", a=17),
            is_filterable=False,
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=True)
        state = sut.update(client, old_state)
        # the new state is None and no put was made
        assert state is None

    def test_update_valueprop_name(self, respx_mock):
        # given a derived property exists with y=2x17
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name1",
                    "dataTypeId": {"scope": "ids", "code": "idc"},
                },
            )
        )
        # value url for the update
        respx_mock.put("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # when we update it with a new name
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name2",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=False)
        state = sut.update(client, old_state)
        # then a put is made with the new formula
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name2",
            "dataTypeId": {"scope": "ids", "code": "idc"},
        }
        # and the state is returned
        assert state is not None

    def test_update_formula(self, respx_mock):
        # given a derived property exists with y=2x17
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name",
                    "dataTypeId": {"scope": "ids", "code": "idc"},
                    "propertyDescription": "description",
                    "derivationFormula": "2 * 17",
                },
            )
        )
        respx_mock.put("/api/api/propertydefinitions/derived/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # when we update it with a new formula
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("3 * {a}", a=26),
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=True)
        state = sut.update(client, old_state)
        # then a put is made with the new formula
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
            "derivationFormula": "3 * 26",
        }
        # and the state is returned
        assert state is not None

    def test_update_is_filterable(self, respx_mock):
        # given a derived property exists with y=2x17
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name",
                    "dataTypeId": {"scope": "ids", "code": "idc"},
                    "propertyDescription": "description",
                    "derivationFormula": "2 * 17",
                    "isFilterable": True,
                },
            )
        )
        respx_mock.put("/api/api/propertydefinitions/derived/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # when we update it with a new formula
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("2 * {a}", a=17),
            is_filterable=False,
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=True)
        state = sut.update(client, old_state)
        # then a put is made with the new formula
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
            "derivationFormula": "2 * 17",
            "isFilterable": False,
        }
        # and the state is returned
        assert state is not None

    def test_update_code(self, respx_mock):
        # delete using normal definition url
        respx_mock.delete("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        # create using derived url
        respx_mock.post("/api/api/propertydefinitions/derived").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # when we update it with a different code
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd2",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("3 * {a}", a=26),
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=True)
        state = sut.update(client, old_state)
        # then a post is made with the new formula
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd2",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
            "derivationFormula": "3 * 26",
        }
        # and the state is returned
        assert state is not None
        # and the delete was called

    def test_update_derived_to_value(self, respx_mock):
        # delete using normal definition url
        respx_mock.delete("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        # create using value url
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # when we update it using the same scope/code but a value property
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=True)
        state = sut.update(client, old_state)
        # then a post is made with the value property
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
        }
        # and the state is returned
        assert state == {"domain": "Holding", "scope": "sc1", "code": "cd1", "derived": False}
        # and the get and the delete are both called

    def test_update_data_type(self, respx_mock):
        # given a property exists with datatype=string
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name",
                    "dataTypeId": {"scope": "system", "code": "string"},
                },
            )
        )
        # delete using normal definition url
        respx_mock.delete("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        # create using value url
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # when we update it using the same scope/code but a number type
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="system", code="number"),
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=False)
        state = sut.update(client, old_state)
        # then a post is made with the new datatype
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "system", "code": "number"},
        }
        # and the state is returned
        assert state == {"domain": "Holding", "scope": "sc1", "code": "cd1", "derived": False}
        # and the get and the delete are both called

    def test_update_constraint_style_recreates(self, respx_mock):
        # given a property exists with the default constraint style
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name",
                    "dataTypeId": {"scope": "system", "code": "string"},
                },
            )
        )
        # delete using normal definition url
        respx_mock.delete("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        # create using value url
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # when we update it using the same values but a modified constraint style
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            constraint_style=property.ConstraintStyle.Identifier,
            data_type_id=property.ResourceId(scope="system", code="string"),
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=False)
        state = sut.update(client, old_state)
        # then the delete is triggered
        # and a post is made with the new constraint
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "constraintStyle": "Identifier",
            "dataTypeId": {"scope": "system", "code": "string"},
        }
        # and the state is returned
        assert state == {"domain": "Holding", "scope": "sc1", "code": "cd1", "derived": False}
        # and the get and the delete are both called

    def test_update_lifetime(self, respx_mock):
        # given a perpetual property
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name",
                    "dataTypeId": {"scope": "system", "code": "string"},
                    "lifeTime": "Perpetual",
                },
            )
        )
        # delete using normal definition url
        respx_mock.delete("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        # create using value url
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # when we update it using the same scope/code but a timevariant lifetime
        sut = property.DefinitionResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="system", code="string"),
            life_time=property.LifeTime.TimeVariant,
        )
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=False)
        state = sut.update(client, old_state)
        # then a post is made with the new lifetime

        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "system", "code": "string"},
            "lifeTime": "TimeVariant",
        }
        # and the state is returned
        assert state == {"domain": "Holding", "scope": "sc1", "code": "cd1", "derived": False}
        # and the get and the delete are both called

    def test_delete(self, respx_mock):
        # delete using normal definition url
        respx_mock.delete("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # when we delete a property definition
        old_state = SimpleNamespace(domain="Holding", scope="sc1", code="cd1", derived=False)
        property.DefinitionResource.delete(client, old_state)
        # then the delete call is made

    def test_dump(self):
        # given a datatype resource
        dt = datatype.DataTypeResource(
            id="dt1",
            scope="dtscope",
            code="dtcode",
            type_value_range=datatype.TypeValueRange.CLOSED,
            display_name="Priority Test",
            description="A test datatype for Priority",
            value_type=datatype.ValueType.STRING,
            acceptable_values=["High", "Medium", "Low"],
        )
        # and a property definition with derivation formula
        prop_ref = property.DefinitionRef(
            id="prop1", domain=property.Domain.Instrument, scope="refs", code="x"
        )
        sut = property.DefinitionResource(
            id="def1",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="Test Property",
            data_type_id=dt,
            property_description="A test property description",
            derivation_formula=property.Formula("2 * {a}", a=prop_ref),
        )
        # when we dump it
        dumped = sut.model_dump(
            by_alias=True, round_trip=True, exclude_none=True, context={"style": "dump"}
        )
        # then the datatype and the formula args have been ref'd
        assert dumped == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "Test Property",
            "dataTypeId": {"$ref": "dt1"},
            "propertyDescription": "A test property description",
            "derivationFormula": {"formula": "2 * {a}", "args": {"a": {"$ref": "prop1"}}},
        }

    def test_dump_value_property(self):
        # given a datatype reference
        dt_ref = property.ResourceId(scope="system", code="string")
        # and a value property definition (no derivation formula)
        sut = property.DefinitionResource(
            id="def2",
            domain=property.Domain.Holding,
            scope="sc2",
            code="cd2",
            display_name="Value Property",
            data_type_id=dt_ref,
            property_description="A test value property",
            life_time=property.LifeTime.Perpetual,
            constraint_style=property.ConstraintStyle.Property,
        )
        # when we dump it
        dumped = sut.model_dump(
            by_alias=True, round_trip=True, exclude_none=True, context={"style": "dump"}
        )
        # then the dumped state is correct and data_type_id is not converted to $ref for ResourceId
        assert dumped == {
            "domain": "Holding",
            "scope": "sc2",
            "code": "cd2",
            "displayName": "Value Property",
            "dataTypeId": {"scope": "system", "code": "string"},
            "propertyDescription": "A test value property",
            "lifeTime": "Perpetual",
            "constraintStyle": "Property",
        }

    def test_undump(self):
        # given a datatype resource and property ref for references
        dt = datatype.DataTypeResource(
            id="dt1",
            scope="dtscope",
            code="dtcode",
            type_value_range=datatype.TypeValueRange.CLOSED,
            display_name="Priority Test",
            description="A test datatype for Priority",
            value_type=datatype.ValueType.STRING,
            acceptable_values=["High", "Medium", "Low"],
        )
        prop_ref = property.DefinitionRef(
            id="prop1", domain=property.Domain.Instrument, scope="refs", code="x"
        )
        # and a dumped derived property state
        dumped = {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "Test Property",
            "dataTypeId": {"$ref": "dt1"},
            "propertyDescription": "A test property description",
            "derivationFormula": {"formula": "2 * {a}", "args": {"a": {"$ref": "prop1"}}},
        }
        # when we undump it
        sut = property.DefinitionResource.model_validate(
            dumped, context={"style": "undump", "$refs": {"dt1": dt, "prop1": prop_ref}, "id": "def1"}
        )
        # then the id has been extracted from the context
        assert sut.id == "def1"
        assert sut.domain == property.Domain.Holding
        assert sut.scope == "sc1"
        assert sut.code == "cd1"
        assert sut.display_name == "Test Property"
        assert sut.property_description == "A test property description"
        # and the datatype ref has been wired up
        assert sut.data_type_id == dt
        # and the derivation formula is correct with resolved references
        assert sut.derivation_formula
        assert sut.derivation_formula.formula == "2 * {a}"
        assert sut.derivation_formula.args == {"a": prop_ref}

    def test_undump_value_property(self):
        # given a dumped value property state
        dumped = {
            "domain": "Holding",
            "scope": "sc2",
            "code": "cd2",
            "displayName": "Value Property",
            "dataTypeId": {"scope": "system", "code": "string"},
            "propertyDescription": "A test value property",
            "lifeTime": "Perpetual",
            "constraintStyle": "Property",
        }
        # when we undump it
        sut = property.DefinitionResource.model_validate(
            dumped, context={"style": "undump", "$refs": {}, "id": "def2"}
        )
        # then the id has been extracted from the context
        assert sut.id == "def2"
        assert sut.domain == property.Domain.Holding
        assert sut.scope == "sc2"
        assert sut.code == "cd2"
        assert sut.display_name == "Value Property"
        assert sut.property_description == "A test value property"
        assert sut.life_time == property.LifeTime.Perpetual
        assert sut.constraint_style == property.ConstraintStyle.Property
        # and the datatype is a ResourceId object
        assert isinstance(sut.data_type_id, property.ResourceId)
        assert sut.data_type_id.scope == "system"
        assert sut.data_type_id.code == "string"
        # and no derivation formula
        assert sut.derivation_formula is None

    def test_from_api_plain(self):
        # given a datatype in api format
        dtapi = {
            "id": {"scope": "ids", "code": "idc"},
            "typeValueRange": "Open",
            "displayName": "displayName 1",
            "description": "description one",
            "valueType": "String",
            "unitSchema": "NoUnits",
        }
        # given a property in actual api format
        pdapi = {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
        }
        # when we parse them both
        dtj = datatype.DataTypeResource.from_api(dtapi)
        pdj = property.DefinitionResource.from_api(pdapi)
        # the property fields are set
        assert pdj["scope"] == "sc1"
        assert pdj["id"] == "definition_holding_sc1_cd1"
        assert pdj["dataTypeId"] == {"$ref": "datatype_ids_idc"}
        # if we deserialise the datatype
        context = {}
        dtr = datatype.DataTypeResource.model_validate(dtj, context=context)
        # and add it to the context
        context = {"$refs": {dtr.id: dtr}}
        # then deserialise the propdef using the context
        pdr = property.DefinitionResource.model_validate(pdj, context=context)
        # then the propdef and datatype are connected
        assert pdr.data_type_id == dtr

    def test_from_api_derived(self):
        # given a derived property in API format
        example = {
            "id": "one",
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name",
            "dataTypeId": {"scope": "ids", "code": "idc"},
            "propertyDescription": "description",
            "derivationFormula": "3 + Properties[Holding/sc2/cd2]"
        }
        dtref = datatype.DataTypeRef(id="datatype_ids_idc", scope="ids", code="idc")
        pdref = property.DefinitionRef(
            id="definition_holding_sc2_cd2",
            domain=property.Domain.Holding,
            scope="sc2",
            code="cd2",
        )
        # when we parse it
        pdj = property.DefinitionResource.from_api(example)
        # the property fields are set
        assert pdj["scope"] == "sc1"
        assert pdj["id"] == "definition_holding_sc1_cd1"
        assert pdj["dataTypeId"] == {"$ref": "datatype_ids_idc"}
        # and the formula args are extracted
        assert pdj["derivationFormula"]
        args = pdj["derivationFormula"]["args"]
        assert args
        assert len(args) == 1
        # and the value of the arg is a reference to another property
        assert args["p1"] == {"$ref": "definition_holding_sc2_cd2"}
        # when we deserialise
        context = {
            "$refs": {
                dtref.id: dtref,
                pdref.id: pdref,
            }
        }
        pdr = property.DefinitionResource.model_validate(pdj, context=context)
        assert pdr

    @pytest.mark.asyncio
    @pytest.mark.respx(base_url=TEST_BASE)
    async def test_list(self, respx_mock, definition_resource):
        # GIVEN a mocked list endpoint
        mock_request(
            respx_mock,
            property.DefinitionResource.list_endpoint(),
            definition_resource
        )
        async_client = httpx.AsyncClient(base_url=TEST_BASE)
        # WHEN calling the list method with query params
        result = await property.DefinitionResource.list(
            async_client,
            query_params={
                "limit": 10,
                "page": "next_page"
            }
        )
        # AND the resource can be validated with proper context
        dt_ref = datatype.DataTypeRef(id="datatype_system_number", scope="system", code="number")
        context = {"$refs": {dt_ref.id: dt_ref}}
        resource = property.DefinitionResource.model_validate(
            result["values"][0]["value"],
            context=context
        )
        assert resource.id == "definition_instrument_lumi-prop-test-scope_lumi-prop-test-code-num"
        assert resource.scope == "lumi-prop-test-scope"
        assert resource.code == "lumi-prop-test-code-num"
        assert resource.data_type_id == dt_ref
        # AND the correct request was made
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == "/api/api/propertydefinitions/$list"
        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, definition_resource):
        # GIVEN a Definition resource with specific domain, scope and code
        resource = definition_resource["values"][0]
        domain = resource["domain"]
        scope = resource["scope"]
        code = resource["code"]
        # AND the resource exists in LUSID
        respx_mock.get(
            f"/api/api/propertydefinitions/{domain}/{scope}/{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 property.DefinitionResource.fetch(
            async_client,
            keys={"domain": domain, "scope": scope, "code": code}
        )
        # THEN the response is as expected
        assert response == {
            "valueType": resource["valueType"],
            "displayName": resource["displayName"],
            "dataTypeId": {
                "$ref": f"datatype_{resource['dataTypeId']['scope']}_{resource['dataTypeId']['code']}"
            },
            "type": resource["type"],
            "unitSchema": resource["unitSchema"],
            "domain": domain,
            "scope": scope,
            "code": code,
            "lifeTime": resource["lifeTime"],
            "constraintStyle": resource["constraintStyle"],
            "propertyDescription": resource["propertyDescription"],
            "collectionType": resource["collectionType"],
            "customEntityTypes": resource["customEntityTypes"],
            "valueFormat": resource["valueFormat"],
            "id": f"definition_instrument_{scope}_{code}"
        }
        # AND the correct request was made to LUSID
        request = respx_mock.calls.last.request
        assert request.method == "GET"
        assert request.url.path == f"/api/api/propertydefinitions/{domain}/{scope}/{code}"
        assert request.url.query == b""

    def test_formula_property_ref_uses_definition_prefix(self):
        formula = "Properties[Holding/default/TestProp]"
        parsed = property.Formula.from_api(formula)
        assert parsed is not None
        assert parsed["args"]["p1"]["$ref"] == "definition_holding_default_testprop"

    def test_formula_property_definition_is_resolved(self):
        # --- GIVEN: a base property that will be referenced in the formula ---
        base_prop = property.DefinitionResource(
            id="definition_holding_default_BaseProp",
            domain=property.Domain.Holding,
            scope="default",
            code="BaseProp",
            display_name="Base",
            data_type_id=datatype.ResourceId(scope="system", code="string"),
        )

        # --- AND: a formula string that references the base property ---
        parsed = property.Formula.from_api("Properties[Holding/default/BaseProp] + 1")
        assert parsed is not None
        # --- WHEN: create a Formula object with actual property objects instead of $refs ---

        formula = property.Formula(
            formula=parsed["formula"],
            **{k: base_prop for k in parsed["args"]}  # map p1 -> base_prop
        )

        # --- AND: create a derived property using this formula ---
        derived = property.DefinitionResource(
            id="definition_holding_default_DerivedProp",
            domain=property.Domain.Holding,
            scope="default",
            code="Derived",
            display_name="Derived",
            data_type_id=datatype.ResourceId(scope="system", code="string"),
            derivation_formula=formula,
        )

        # --- THEN: deps() should include the base property ---
        deps = derived.deps()
        assert base_prop in deps


@pytest.mark.respx(base_url=TEST_BASE)
class DescribeDefinitionOverrideResource:
    client = httpx.Client(base_url=TEST_BASE, event_hooks={"response": [lambda x: x.raise_for_status()]})

    def test_create_when_not_exists(self, respx_mock):
        # given no existing property
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                404,
                json={
                },
            )
        )
        # mock the create
        respx_mock.post("/api/api/propertydefinitions").mock(return_value=httpx.Response(200, json={}))
        client = self.client
        # when we create a desired override
        sut = property.DefinitionOverrideResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name2",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
        )
        state = sut.create(client)
        # then state is returned with pre-existing false because it didn't exist
        assert state is not None
        assert state == {
            "code": "cd1",
            "derived": False,
            "domain": "Holding",
            "preexisting": False,
            "scope": "sc1"
        }

    def test_create_when_already_exists(self, respx_mock):
        # given it already exists
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name",
                    "dataTypeId": {"scope": "ids", "code": "idc"},
                    "propertyDescription": "description",
                    "derivationFormula": "2 * 17",
                    "isFilterable": False,
                },
            )
        )
        # mock the update
        respx_mock.put("/api/api/propertydefinitions/derived/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # when we create it but with a new formula
        sut = property.DefinitionOverrideResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
            property_description="description",
            derivation_formula=property.Formula("100 * {a}", a=17),
            is_filterable=False,
        )
        state = sut.create(client)
        # then state is returned with pre-existing true
        assert state is not None
        assert state == {
            "code": "cd1",
            "derived": True,
            "domain": "Holding",
            "preexisting": True,
            "scope": "sc1",
        }
        # and update was called
        req = respx_mock.calls.last.request
        assert req.method == "PUT"

    def test_update_preserves_preexisting(self, respx_mock):
        # given a derived property exists with y=2x17
        respx_mock.get("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(
                200,
                json={
                    "domain": "Holding",
                    "scope": "sc1",
                    "code": "cd1",
                    "displayName": "name1",
                    "dataTypeId": {"scope": "ids", "code": "idc"},
                },
            )
        )
        # value url for the update
        respx_mock.put("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # when we update it with a new name
        sut = property.DefinitionOverrideResource(
            id="one",
            domain=property.Domain.Holding,
            scope="sc1",
            code="cd1",
            display_name="name2",
            data_type_id=property.ResourceId(scope="ids", code="idc"),
        )
        old_state = SimpleNamespace(
            domain="Holding", scope="sc1", code="cd1", derived=False, preexisting=True
        )
        state = sut.update(client, old_state)
        # then a put is made with the new formula
        req = respx_mock.calls.last.request
        assert json.loads(req.content) == {
            "domain": "Holding",
            "scope": "sc1",
            "code": "cd1",
            "displayName": "name2",
            "dataTypeId": {"scope": "ids", "code": "idc"},
        }
        # and the state is returned preserving the preexisting state
        assert state is not None
        assert state["preexisting"]

    def test_deletes_if_created(self, respx_mock):
        # mock delete
        respx_mock.delete("/api/api/propertydefinitions/Holding/sc1/cd1").mock(
            return_value=httpx.Response(200, json={})
        )
        client = self.client
        # when we delete and it is we created it (not preexisting)
        old_state = SimpleNamespace(
            domain="Holding", scope="sc1", code="cd1", derived=False, preexisting=False
        )
        property.DefinitionOverrideResource.delete(client, old_state)
        # then the delete call is made
        req = respx_mock.calls.last.request
        assert req.method == "DELETE"

    def test_no_deletes_if_preexisting(self, respx_mock):
        client = self.client
        # when we delete and it is we created it (not preexisting)
        old_state = SimpleNamespace(
            domain="Holding", scope="sc1", code="cd1", derived=False, preexisting=True
        )
        property.DefinitionOverrideResource.delete(client, old_state)
