import pytest
from cognite.client.data_classes.capabilities import (
    AssetsAcl,
    Capability,
    DataModelInstancesAcl,
    DataModelsAcl,
    EventsAcl,
    ExtractionPipelinesAcl,
    FilesAcl,
    LabelsAcl,
    ProjectCapability,
    ProjectCapabilityList,
    RelationshipsAcl,
    SequencesAcl,
    ThreeDAcl,
    TimeSeriesAcl,
    TransformationsAcl,
    WorkflowOrchestrationAcl,
)
from cognite.client.data_classes.iam import TokenInspection

from cognite_toolkit._cdf_tk.client.testing import monkeypatch_toolkit_client
from cognite_toolkit._cdf_tk.exceptions import AuthorizationError
from cognite_toolkit._cdf_tk.utils.validate_access import ValidateAccess


class TestValidateAccess:
    @pytest.mark.parametrize(
        "capabilities, spaces, expected_error",
        [
            pytest.param(
                [],
                None,
                "You have no permission to read data models. This is required to test the operation.",
                id="No capabilities",
            ),
            pytest.param(
                [DataModelsAcl([DataModelsAcl.Action.Read], DataModelsAcl.Scope.SpaceID(["space1"]))],
                {"space2"},
                "You have no permission to read the 'space2' space(s). This is required to test the operation.",
                id="Space mismatch",
            ),
        ],
    )
    def test_model_access_raise(
        self, capabilities: list[Capability], spaces: set[str] | None, expected_error: str
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            validator = ValidateAccess(client, "test the operation")
            with pytest.raises(AuthorizationError) as exc:
                validator.data_model(["read"], spaces=spaces)
            assert str(exc.value) == expected_error

    @pytest.mark.parametrize(
        "capabilities, spaces, expected_result",
        [
            pytest.param(
                [DataModelsAcl([DataModelsAcl.Action.Read], DataModelsAcl.Scope.SpaceID(["space1"]))],
                {"space1"},
                None,
                id="Space match",
            ),
            pytest.param(
                [DataModelsAcl([DataModelsAcl.Action.Read], DataModelsAcl.Scope.All())], None, None, id="All scope"
            ),
            pytest.param(
                [DataModelsAcl([DataModelsAcl.Action.Read], DataModelsAcl.Scope.SpaceID(["space1", "space2"]))],
                None,
                ["space1", "space2"],
                id="Limited list of spaces",
            ),
        ],
    )
    def test_model_access(
        self, capabilities: list[Capability], spaces: set[str] | None, expected_result: list[str] | None
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            validator = ValidateAccess(client, "test the operation")
            result = validator.data_model(["read"], spaces=spaces)
            assert result == expected_result

    @pytest.mark.parametrize(
        "capabilities, spaces, expected_error",
        [
            pytest.param(
                [],
                None,
                "You have no permission to read instances. This is required to test the operation.",
                id="No capabilities",
            ),
            pytest.param(
                [
                    DataModelInstancesAcl(
                        [DataModelInstancesAcl.Action.Read], DataModelInstancesAcl.Scope.SpaceID(["space1"])
                    )
                ],
                {"space2"},
                "You have no permission to read instances in the 'space2' space(s). This is required to test the operation instances.",
                id="Space mismatch",
            ),
        ],
    )
    def test_instances_access_raise(
        self, capabilities: list[Capability], spaces: set[str] | None, expected_error: str
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            validator = ValidateAccess(client, "test the operation")
            with pytest.raises(AuthorizationError) as exc:
                validator.instances(["read"], spaces=spaces)
            assert str(exc.value) == expected_error

    @pytest.mark.parametrize(
        "capabilities, spaces, expected_result",
        [
            pytest.param(
                [
                    DataModelInstancesAcl(
                        [DataModelInstancesAcl.Action.Read], DataModelInstancesAcl.Scope.SpaceID(["space1"])
                    )
                ],
                {"space1"},
                None,
                id="Space match",
            ),
            pytest.param(
                [DataModelInstancesAcl([DataModelInstancesAcl.Action.Read], DataModelInstancesAcl.Scope.All())],
                None,
                None,
                id="All scope",
            ),
            pytest.param(
                [
                    DataModelInstancesAcl(
                        [DataModelInstancesAcl.Action.Read], DataModelInstancesAcl.Scope.SpaceID(["space1", "space2"])
                    )
                ],
                None,
                ["space1", "space2"],
                id="Limited list of spaces",
            ),
        ],
    )
    def test_instances_access(
        self, capabilities: list[Capability], spaces: set[str] | None, expected_result: list[str] | None
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            validator = ValidateAccess(client, "test the operation")
            result = validator.instances(["read"], spaces=spaces)
            assert result == expected_result

    @pytest.mark.parametrize(
        "capabilities, dataset_ids, expected_error",
        [
            pytest.param(
                [],
                None,
                "You have no permission to read time series. This is required to test the operation.",
                id="No capabilities",
            ),
            pytest.param(
                [TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.DataSet([1]))],
                {2},
                "You have no permission to read time series in dataset 2. This is required to test the operation.",
                id="Dataset mismatch",
            ),
            pytest.param(
                [TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.AssetRootID([10]))],
                {1},
                "You have no permission to read time series in dataset 1. This is required to test the operation.",
                id="Access by other scope type",
            ),
        ],
    )
    def test_timeseries_access_raise(
        self, capabilities: list[Capability], dataset_ids: set[int] | None, expected_error: str
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        def external_id_lookup(ids: list[int]) -> list[str]:
            return [str(id_) for id_ in ids]

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            client.lookup.data_sets.external_id.side_effect = external_id_lookup
            client.lookup.assets.external_id.side_effect = external_id_lookup
            client.lookup.time_series.external_id.side_effect = external_id_lookup

            validator = ValidateAccess(client, "test the operation")
            with pytest.raises(AuthorizationError) as exc:
                validator.timeseries(["read"], dataset_ids=dataset_ids)
            assert str(exc.value) == expected_error

    @pytest.mark.parametrize(
        "capabilities, dataset_ids, expected_result",
        [
            pytest.param(
                [TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.DataSet([1, 2]))],
                {1},
                None,
                id="Dataset match",
            ),
            pytest.param(
                [TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.All())],
                None,
                None,
                id="All scope",
            ),
            pytest.param(
                [TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.DataSet([1, 2]))],
                None,
                {"dataset": ["1", "2"]},
                id="Limited list of datasets",
            ),
            pytest.param(
                [
                    TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.AssetRootID([10, 20])),
                    TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.DataSet([15, 25])),
                    TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.ID([100, 200])),
                ],
                None,
                {
                    "asset root": ["10", "20"],
                    "dataset": ["15", "25"],
                    "time series": ["100", "200"],
                },
                id="Multiple scopes with IDs",
            ),
        ],
    )
    def test_timeseries_access(
        self, capabilities: list[Capability], dataset_ids: set[int] | None, expected_result: dict | None
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        def external_id_lookup(ids: list[int]) -> list[str]:
            return [str(id_) for id_ in ids]

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            client.lookup.data_sets.external_id.side_effect = external_id_lookup
            client.lookup.assets.external_id.side_effect = external_id_lookup
            client.lookup.time_series.external_id.side_effect = external_id_lookup

            validator = ValidateAccess(client, "test the operation")
            result = validator.timeseries(["read"], dataset_ids=dataset_ids)
            assert result == expected_result

    @pytest.mark.parametrize(
        "capabilities, dataset_ids, expected_error",
        [
            pytest.param(
                [],
                None,
                "You have no permission to read files. This is required to test the operation.",
                id="No capabilities",
            ),
            pytest.param(
                [FilesAcl([FilesAcl.Action.Read], FilesAcl.Scope.DataSet([1]))],
                {2},
                "You have no permission to read files in dataset 2. This is required to test the operation.",
                id="Dataset mismatch",
            ),
        ],
    )
    def test_files_access_raise(
        self, capabilities: list[Capability], dataset_ids: set[int] | None, expected_error: str
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        def external_id_lookup(ids: list[int]) -> list[str]:
            return [str(id_) for id_ in ids]

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            client.lookup.data_sets.external_id.side_effect = external_id_lookup
            validator = ValidateAccess(client, "test the operation")
            with pytest.raises(AuthorizationError) as exc:
                validator.files(["read"], dataset_ids=dataset_ids)
            assert str(exc.value) == expected_error

    @pytest.mark.parametrize(
        "capabilities, dataset_ids, expected_result",
        [
            pytest.param(
                [FilesAcl([FilesAcl.Action.Read], FilesAcl.Scope.DataSet([1, 2]))],
                {1},
                None,
                id="Dataset match",
            ),
            pytest.param(
                [FilesAcl([FilesAcl.Action.Read], FilesAcl.Scope.All())],
                None,
                None,
                id="All scope",
            ),
            pytest.param(
                [FilesAcl([FilesAcl.Action.Read], FilesAcl.Scope.DataSet([1, 2]))],
                None,
                {"dataset": ["1", "2"]},
                id="Limited list of datasets",
            ),
        ],
    )
    def test_files_access(
        self, capabilities: list[Capability], dataset_ids: set[int] | None, expected_result: dict | None
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        def external_id_lookup(ids: list[int]) -> list[str]:
            return [str(id_) for id_ in ids]

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            client.lookup.data_sets.external_id.side_effect = external_id_lookup
            validator = ValidateAccess(client, "test the operation")
            result = validator.files(["read"], dataset_ids=dataset_ids)
            assert result == expected_result

    @pytest.mark.parametrize(
        "capabilities, dataset_ids, expected_error",
        [
            pytest.param(
                [],
                None,
                "You have no permission to read assets. This is required to test the operation.",
                id="No capabilities",
            ),
            pytest.param(
                [AssetsAcl([AssetsAcl.Action.Read], AssetsAcl.Scope.DataSet([1]))],
                {1, 2},
                "You have no permission to read assets in dataset(s) 2. This is required to test the operation.",
                id="Missing dataset",
            ),
        ],
    )
    def test_assets_access_raise(
        self, capabilities: list[Capability], dataset_ids: set[int] | None, expected_error: str
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        def external_id_lookup(ids: list[int]) -> list[str]:
            return [str(id_) for id_ in ids]

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            client.lookup.data_sets.external_id.side_effect = external_id_lookup
            validator = ValidateAccess(client, "test the operation")
            with pytest.raises(AuthorizationError) as exc:
                validator.assets(["read"], dataset_ids=dataset_ids)
            assert str(exc.value) == expected_error

    @pytest.mark.parametrize(
        "capabilities, dataset_ids, expected_result",
        [
            pytest.param(
                [AssetsAcl([AssetsAcl.Action.Read], AssetsAcl.Scope.DataSet([1, 2]))],
                {1},
                None,
                id="Dataset match",
            ),
            pytest.param(
                [AssetsAcl([AssetsAcl.Action.Read], AssetsAcl.Scope.All())],
                None,
                None,
                id="All scope",
            ),
            pytest.param(
                [AssetsAcl([AssetsAcl.Action.Read], AssetsAcl.Scope.DataSet([1, 2]))],
                None,
                {"dataset": ["1", "2"]},
                id="Limited list of datasets",
            ),
        ],
    )
    def test_assets_access(
        self, capabilities: list[Capability], dataset_ids: set[int] | None, expected_result: dict | None
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)

        def external_id_lookup(ids: list[int]) -> list[str]:
            return [str(id_) for id_ in ids]

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            client.lookup.data_sets.external_id.side_effect = external_id_lookup
            validator = ValidateAccess(client, "test the operation")
            result = validator.assets(["read"], dataset_ids=dataset_ids)
            assert result == expected_result

    @pytest.mark.parametrize(
        "capabilities, dataset_ids, expected_result",
        [
            pytest.param(
                [
                    AssetsAcl([AssetsAcl.Action.Read], AssetsAcl.Scope.DataSet([1, 2])),
                    FilesAcl([FilesAcl.Action.Read], FilesAcl.Scope.DataSet([1, 2])),
                    TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.DataSet([1])),
                    EventsAcl([EventsAcl.Action.Read], EventsAcl.Scope.DataSet([1])),
                    RelationshipsAcl([RelationshipsAcl.Action.Read], RelationshipsAcl.Scope.DataSet([1, 2])),
                    SequencesAcl([SequencesAcl.Action.Read], SequencesAcl.Scope.DataSet([1, 2])),
                    ThreeDAcl([ThreeDAcl.Action.Read], ThreeDAcl.Scope.DataSet([1])),
                    LabelsAcl([LabelsAcl.Action.Read], LabelsAcl.Scope.DataSet([1])),
                ],
                {1},
                None,
                id="Dataset match",
            ),
            pytest.param(
                [
                    AssetsAcl([AssetsAcl.Action.Read], AssetsAcl.Scope.All()),
                    FilesAcl([FilesAcl.Action.Read], FilesAcl.Scope.All()),
                    TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.All()),
                    EventsAcl([EventsAcl.Action.Read], EventsAcl.Scope.All()),
                    RelationshipsAcl([RelationshipsAcl.Action.Read], RelationshipsAcl.Scope.All()),
                    SequencesAcl([SequencesAcl.Action.Read], SequencesAcl.Scope.All()),
                    ThreeDAcl([ThreeDAcl.Action.Read], ThreeDAcl.Scope.All()),
                    LabelsAcl([LabelsAcl.Action.Read], LabelsAcl.Scope.All()),
                ],
                None,
                None,
                id="All scope",
            ),
            pytest.param(
                [
                    AssetsAcl([AssetsAcl.Action.Read], AssetsAcl.Scope.DataSet([1, 2])),
                    FilesAcl([FilesAcl.Action.Read], FilesAcl.Scope.DataSet([1, 3])),
                    TimeSeriesAcl([TimeSeriesAcl.Action.Read], TimeSeriesAcl.Scope.DataSet([1, 4])),
                    EventsAcl([EventsAcl.Action.Read], EventsAcl.Scope.DataSet([1, 2])),
                    RelationshipsAcl([RelationshipsAcl.Action.Read], RelationshipsAcl.Scope.DataSet([1, 2])),
                    SequencesAcl([SequencesAcl.Action.Read], SequencesAcl.Scope.DataSet([1, 9])),
                    ThreeDAcl([ThreeDAcl.Action.Read], ThreeDAcl.Scope.DataSet([1, 3])),
                    LabelsAcl([LabelsAcl.Action.Read], LabelsAcl.Scope.DataSet([1, 3])),
                ],
                None,
                {
                    "3D models": [1, 3],
                    "assets": [1, 2],
                    "events": [1, 2],
                    "files": [1, 3],
                    "labels": [1, 3],
                    "relationships": [1, 2],
                    "sequences": [1, 9],
                    "time series": [1, 4],
                },
                id="Limited list of datasets",
            ),
        ],
    )
    def test_dataset_data_access(
        self, capabilities: list[Capability], dataset_ids: set[int] | None, expected_result: dict | None
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)
        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            validator = ValidateAccess(client, "test the operation")
            result = validator.dataset_data(["read"], dataset_ids=dataset_ids)
            assert result == expected_result

    @pytest.mark.parametrize(
        "capabilities, expected_error",
        [
            pytest.param(
                [],
                (
                    "You have no permission to read 3D models, assets, events, files, labels, "
                    "relationships, sequences and time series on datasets 1 and 2. This is "
                    "required to test the operation."
                ),
                id="No capabilities",
            ),
            pytest.param(
                [
                    AssetsAcl([AssetsAcl.Action.Read], AssetsAcl.Scope.All()),
                    FilesAcl([FilesAcl.Action.Read], FilesAcl.Scope.All()),
                ],
                (
                    "You have no permission to read 3D models, events, labels, relationships, "
                    "sequences and time series on datasets 1 and 2. This is required to test the "
                    "operation."
                ),
                id="Partial capabilities",
            ),
        ],
    )
    def test_dataset_no_data_access(self, capabilities: list[Capability], expected_error: str) -> None:
        inspection = self._create_inspection_obj(capabilities)

        def external_id_lookup(ids: list[int]) -> list[str]:
            return [str(id_) for id_ in ids]

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            client.lookup.data_sets.external_id.side_effect = external_id_lookup
            validator = ValidateAccess(client, "test the operation")
            with pytest.raises(AuthorizationError) as exc:
                validator.dataset_data(["read"], dataset_ids={1, 2})
            assert str(exc.value) == expected_error

    @pytest.mark.parametrize(
        "capabilities, dataset_ids, expected_result",
        [
            pytest.param(
                [
                    TransformationsAcl([TransformationsAcl.Action.Read], TransformationsAcl.Scope.DataSet([1, 2])),
                    WorkflowOrchestrationAcl(
                        [WorkflowOrchestrationAcl.Action.Read], WorkflowOrchestrationAcl.Scope.DataSet([1, 2])
                    ),
                    ExtractionPipelinesAcl(
                        [ExtractionPipelinesAcl.Action.Read], ExtractionPipelinesAcl.Scope.DataSet([1])
                    ),
                ],
                {1},
                None,
                id="Dataset match",
            ),
            pytest.param(
                [
                    TransformationsAcl([TransformationsAcl.Action.Read], TransformationsAcl.Scope.All()),
                    WorkflowOrchestrationAcl(
                        [WorkflowOrchestrationAcl.Action.Read], WorkflowOrchestrationAcl.Scope.All()
                    ),
                    ExtractionPipelinesAcl([ExtractionPipelinesAcl.Action.Read], ExtractionPipelinesAcl.Scope.All()),
                ],
                None,
                None,
                id="All scope",
            ),
            pytest.param(
                [
                    TransformationsAcl([TransformationsAcl.Action.Read], TransformationsAcl.Scope.DataSet([1, 2])),
                    WorkflowOrchestrationAcl(
                        [WorkflowOrchestrationAcl.Action.Read], WorkflowOrchestrationAcl.Scope.DataSet([1, 3])
                    ),
                    ExtractionPipelinesAcl(
                        [ExtractionPipelinesAcl.Action.Read], ExtractionPipelinesAcl.Scope.DataSet([1, 4])
                    ),
                ],
                None,
                {
                    "transformations": [1, 2],
                    "workflows": [1, 3],
                    "extraction pipelines": [1, 4],
                },
                id="Limited list of datasets",
            ),
        ],
    )
    def test_dataset_configuration_access(
        self, capabilities: list[Capability], dataset_ids: set[int] | None, expected_result: dict | None
    ) -> None:
        inspection = self._create_inspection_obj(capabilities)
        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            validator = ValidateAccess(client, "test the operation")
            result = validator.dataset_configurations(["read"], dataset_ids=dataset_ids)
            assert result == expected_result

    @pytest.mark.parametrize(
        "capabilities, expected_error",
        [
            pytest.param(
                [],
                (
                    "You have no permission to read extraction pipelines, transformations "
                    "and workflows on datasets 1 and 2. This is required to test the operation."
                ),
                id="No capabilities",
            ),
            pytest.param(
                [
                    TransformationsAcl([TransformationsAcl.Action.Read], TransformationsAcl.Scope.All()),
                ],
                (
                    "You have no permission to read extraction pipelines and workflows on datasets 1 and 2. "
                    "This is required to test the operation."
                ),
                id="Partial capabilities",
            ),
            pytest.param(
                [
                    TransformationsAcl([TransformationsAcl.Action.Read], TransformationsAcl.Scope.All()),
                    WorkflowOrchestrationAcl(
                        [WorkflowOrchestrationAcl.Action.Read], WorkflowOrchestrationAcl.Scope.All()
                    ),
                    ExtractionPipelinesAcl(
                        [ExtractionPipelinesAcl.Action.Read], ExtractionPipelinesAcl.Scope.DataSet([1])
                    ),
                ],
                (
                    "You have no permission to read extraction pipelines on datasets 1 and 2. "
                    "This is required to test the operation."
                ),
                id="Missing dataset access",
            ),
        ],
    )
    def test_dataset_no_configuration_access(self, capabilities: list[Capability], expected_error: str) -> None:
        inspection = self._create_inspection_obj(capabilities)

        def external_id_lookup(ids: list[int]) -> list[str]:
            return [str(id_) for id_ in ids]

        with monkeypatch_toolkit_client() as client:
            client.iam.token.inspect.return_value = inspection
            client.lookup.data_sets.external_id.side_effect = external_id_lookup
            validator = ValidateAccess(client, "test the operation")
            with pytest.raises(AuthorizationError) as exc:
                validator.dataset_configurations(["read"], dataset_ids={1, 2})
            assert str(exc.value) == expected_error

    @staticmethod
    def _create_inspection_obj(capabilities: list[Capability]) -> TokenInspection:
        inspection = TokenInspection(
            "123",
            [],
            ProjectCapabilityList(
                [
                    ProjectCapability(
                        capability=capability,
                        project_scope=ProjectCapability.Scope.All(),
                    )
                    for capability in capabilities
                ]
            ),
        )
        return inspection
