# #region Test.Migration.RiskAssessor.Edge [C:3] [TYPE Module] [SEMANTICS test, risk, assessor, migration, edge]
# @BRIEF Edge case tests for risk assessor — index_by_uuid, extract_owner_identifiers, build_risks, score_risks.
# @RELATION BINDS_TO -> [RiskAssessorModule]

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from unittest.mock import AsyncMock, MagicMock

import pytest

from src.core.migration.risk_assessor import (
    index_by_uuid,
    extract_owner_identifiers,
    build_risks,
    score_risks,
)


class TestIndexByUuid:
    def test_empty_list(self):
        result = index_by_uuid([])
        assert result == {}

    def test_no_uuid_keys(self):
        result = index_by_uuid([{"name": "test"}, {"id": 1}])
        assert result == {}

    def test_with_uuid(self):
        result = index_by_uuid([{"uuid": "abc-123", "name": "test"}])
        assert result["abc-123"]["name"] == "test"

    def test_mixed_items(self):
        result = index_by_uuid([
            {"uuid": "abc", "name": "A"},
            {"uuid": None, "name": "B"},
            {"name": "C"},
            {"uuid": "def", "name": "D"},
        ])
        assert "abc" in result
        assert "def" in result
        assert len(result) == 2


class TestExtractOwnerIdentifiers:
    def test_not_a_list(self):
        result = extract_owner_identifiers("not_a_list")
        assert result == []

    def test_none_input(self):
        result = extract_owner_identifiers(None)
        assert result == []

    def test_empty_list(self):
        result = extract_owner_identifiers([])
        assert result == []

    def test_dict_with_username(self):
        result = extract_owner_identifiers([{"username": "alice"}])
        assert result == ["alice"]

    def test_dict_with_id(self):
        result = extract_owner_identifiers([{"id": 42}])
        assert "42" in result

    def test_scalar_values(self):
        result = extract_owner_identifiers(["alice", "bob", None])
        assert "alice" in result
        assert "bob" in result

    def test_sorts_and_deduplicates(self):
        result = extract_owner_identifiers(["bob", "alice", "bob"])
        assert result == ["alice", "bob"]


class TestBuildRisks:
    @pytest.mark.asyncio
    async def test_overwrite_risks_for_all_types(self):
        source = {"dashboards": [], "charts": [], "datasets": []}
        target = {"dashboards": [], "charts": [], "datasets": []}
        diff = {
            "dashboards": {"update": [{"uuid": "d1", "title": "Dash"}], "create": [], "delete": []},
            "charts": {"update": [{"uuid": "c1", "title": "Chart"}], "create": [], "delete": []},
            "datasets": {"update": [{"uuid": "ds1", "title": "Dataset"}], "create": [], "delete": []},
        }
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (0, [])
        risks = await build_risks(source, target, diff, mock_client)
        overwrites = [r for r in risks if r["code"] == "overwrite_existing"]
        assert len(overwrites) == 3

    @pytest.mark.asyncio
    async def test_missing_datasource_risk(self):
        source = {
            "dashboards": [], "charts": [], "datasets": [
                {"uuid": "ds1", "title": "DS", "database_uuid": "db-missing"}
            ]
        }
        target = {"dashboards": [], "charts": [], "datasets": []}
        diff = {"dashboards": {"update": [], "create": [], "delete": []},
                "charts": {"update": [], "create": [], "delete": []},
                "datasets": {"update": [], "create": [], "delete": []}}
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (0, [{"uuid": "db-existing"}])
        risks = await build_risks(source, target, diff, mock_client)
        ds_missing = [r for r in risks if r["code"] == "missing_datasource"]
        assert len(ds_missing) == 1

    @pytest.mark.asyncio
    async def test_breaking_reference_risk(self):
        source = {
            "dashboards": [], "charts": [
                {"uuid": "c1", "title": "Chart", "dataset_uuid": "ds-missing"}
            ], "datasets": []
        }
        target = {"dashboards": [], "charts": [], "datasets": [{"uuid": "ds-exists"}]}
        diff = {"dashboards": {"update": [], "create": [], "delete": []},
                "charts": {"update": [], "create": [], "delete": []},
                "datasets": {"update": [], "create": [], "delete": []}}
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (0, [])
        risks = await build_risks(source, target, diff, mock_client)
        breaking = [r for r in risks if r["code"] == "breaking_reference"]
        assert len(breaking) == 1

    @pytest.mark.asyncio
    async def test_owner_mismatch_risk(self):
        source = {
            "dashboards": [
                {"uuid": "d1", "title": "Dash", "owners": [{"username": "alice"}]}
            ], "charts": [], "datasets": []
        }
        target = {
            "dashboards": [
                {"uuid": "d1", "title": "Dash", "owners": [{"username": "bob"}]}
            ], "charts": [], "datasets": []
        }
        diff = {"dashboards": {"update": [{"uuid": "d1"}], "create": [], "delete": []},
                "charts": {"update": [], "create": [], "delete": []},
                "datasets": {"update": [], "create": [], "delete": []}}
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (0, [])
        risks = await build_risks(source, target, diff, mock_client)
        owner_mismatch = [r for r in risks if r["code"] == "owner_mismatch"]
        assert len(owner_mismatch) == 1

    @pytest.mark.asyncio
    async def test_owner_mismatch_skipped_when_missing(self):
        """Owner mismatch skipped when source or target object is missing."""
        source = {"dashboards": [], "charts": [], "datasets": []}
        target = {"dashboards": [], "charts": [], "datasets": []}
        diff = {"dashboards": {"update": [{"uuid": "nonexistent"}], "create": [], "delete": []},
                "charts": {"update": [], "create": [], "delete": []},
                "datasets": {"update": [], "create": [], "delete": []}}
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (0, [])
        risks = await build_risks(source, target, diff, mock_client)
        owner_mismatch = [r for r in risks if r["code"] == "owner_mismatch"]
        assert len(owner_mismatch) == 0

    @pytest.mark.asyncio
    async def test_empty_source_objects(self):
        source = {"dashboards": [], "charts": [], "datasets": []}
        target = {"dashboards": [], "charts": [], "datasets": []}
        diff = {"dashboards": {"update": [], "create": [], "delete": []},
                "charts": {"update": [], "create": [], "delete": []},
                "datasets": {"update": [], "create": [], "delete": []}}
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (0, [])
        risks = await build_risks(source, target, diff, mock_client)
        assert risks == []


class TestScoreRisks:
    def test_empty(self):
        result = score_risks([])
        assert result["score"] == 0
        assert result["level"] == "low"
        assert result["items"] == []

    def test_low_score(self):
        result = score_risks([{"severity": "low"}, {"severity": "low"}])
        assert result["score"] == 10
        assert result["level"] == "low"

    def test_medium_score(self):
        result = score_risks([{"severity": "high"}, {"severity": "medium"}])
        assert result["score"] == 35
        assert result["level"] == "medium"

    def test_high_score(self):
        result = score_risks([{"severity": "high"}] * 5)
        assert result["score"] == 100  # capped
        assert result["level"] == "high"

    def test_missing_severity_defaults_low(self):
        result = score_risks([{}])
        assert result["score"] == 5

    def test_capped_at_100(self):
        result = score_risks([{"severity": "high"}] * 10)
        assert result["score"] == 100
# #endregion Test.Migration.RiskAssessor.Edge
