"""Tests for the DriftDiffer — core drift detection engine."""

from __future__ import annotations

import time
from typing import Any

import pytest

from driftguard.core.differ import (
    AttributeDiff,
    DriftDiffer,
    DriftResult,
    ResourceDiff,
    _normalize_value,
    _values_equal,
)
from driftguard.store import DriftCategory, DriftSeverity, DriftStore


# --- Value comparison tests ---


class TestNormalizeValue:
    def test_none(self) -> None:
        assert _normalize_value(None) is None

    def test_bool(self) -> None:
        assert _normalize_value(True) is True
        assert _normalize_value(False) is False

    def test_string_strips_and_lowercases(self) -> None:
        assert _normalize_value("  Hello World  ") == "hello world"

    def test_int_and_float(self) -> None:
        assert _normalize_value(42) == 42
        assert _normalize_value(3.14) == 3.14

    def test_list_sorted(self) -> None:
        result = _normalize_value(["c", "a", "b"])
        assert result == ["a", "b", "c"]

    def test_dict_normalized(self) -> None:
        result = _normalize_value({"B": "Two", "A": "One"})
        assert result == {"A": "one", "B": "two"}


class TestValuesEqual:
    def test_both_none(self) -> None:
        assert _values_equal(None, None) is True

    def test_none_and_empty(self) -> None:
        assert _values_equal(None, "") is True
        assert _values_equal(None, []) is True
        assert _values_equal(None, {}) is True
        assert _values_equal(None, 0) is True
        assert _values_equal(None, False) is True

    def test_same_string(self) -> None:
        assert _values_equal("hello", "HELLO") is True

    def test_different_string(self) -> None:
        assert _values_equal("hello", "world") is False

    def test_same_number(self) -> None:
        assert _values_equal(42, 42) is True

    def test_different_number(self) -> None:
        assert _values_equal(42, 43) is False

    def test_sensitive_always_equal(self) -> None:
        assert _values_equal("<sensitive>", "anything") is True
        assert _values_equal("anything", "<sensitive>") is True

    def test_list_order_independent(self) -> None:
        assert _values_equal(["b", "a"], ["a", "b"]) is True

    def test_list_different(self) -> None:
        assert _values_equal(["a", "b"], ["a", "c"]) is False


# --- AttributeDiff tests ---


class TestAttributeDiff:
    def test_security_relevant(self) -> None:
        diff = AttributeDiff("public_access", True, False, "changed")
        assert diff.is_security_relevant is True

    def test_not_security_relevant(self) -> None:
        diff = AttributeDiff("tags.environment", "dev", "prod", "changed")
        assert diff.is_security_relevant is False

    def test_cost_relevant(self) -> None:
        diff = AttributeDiff("instance_type", "t3.micro", "t3.2xlarge", "changed")
        assert diff.is_cost_relevant is True

    def test_encryption_is_security(self) -> None:
        diff = AttributeDiff("encryption_enabled", True, False, "changed")
        assert diff.is_security_relevant is True

    def test_firewall_is_security(self) -> None:
        diff = AttributeDiff("firewall_rule.ingress", [], ["0.0.0.0/0"], "changed")
        assert diff.is_security_relevant is True

    def test_sku_is_cost(self) -> None:
        diff = AttributeDiff("sku.name", "Standard_LRS", "Premium_LRS", "changed")
        assert diff.is_cost_relevant is True


# --- ResourceDiff tests ---


class TestResourceDiff:
    def test_deleted_is_high_severity(self) -> None:
        diff = ResourceDiff(
            resource_address="azurerm_storage_account.main",
            resource_type="azurerm_storage_account",
            resource_id="/subscriptions/xxx/storage/main",
            resource_name="mainstorage",
            provider="azure",
            diff_type="deleted",
        )
        assert diff.severity == DriftSeverity.HIGH

    def test_unmanaged_is_medium(self) -> None:
        diff = ResourceDiff(
            resource_address="extra_vm",
            resource_type="aws_instance",
            resource_id="i-12345",
            resource_name="extra-vm",
            provider="aws",
            diff_type="unmanaged",
        )
        assert diff.severity == DriftSeverity.MEDIUM

    def test_security_drift_is_critical(self) -> None:
        diff = ResourceDiff(
            resource_address="aws_security_group.web",
            resource_type="aws_security_group",
            resource_id="sg-123",
            resource_name="web-sg",
            provider="aws",
            diff_type="modified",
            attribute_diffs=[
                AttributeDiff("ingress.cidr_blocks", ["10.0.0.0/8"], ["0.0.0.0/0"], "changed"),
            ],
        )
        assert diff.has_security_drift is True
        assert diff.severity == DriftSeverity.CRITICAL
        assert diff.category == DriftCategory.SECURITY

    def test_cost_drift_is_high(self) -> None:
        diff = ResourceDiff(
            resource_address="aws_instance.web",
            resource_type="aws_instance",
            resource_id="i-123",
            resource_name="web",
            provider="aws",
            diff_type="modified",
            attribute_diffs=[
                AttributeDiff("instance_type", "t3.micro", "r6g.16xlarge", "changed"),
            ],
        )
        assert diff.has_cost_drift is True
        assert diff.severity == DriftSeverity.HIGH
        assert diff.category == DriftCategory.COST

    def test_config_drift_small(self) -> None:
        diff = ResourceDiff(
            resource_address="azurerm_resource_group.main",
            resource_type="azurerm_resource_group",
            resource_id="/subscriptions/xxx/rg/main",
            resource_name="main-rg",
            provider="azure",
            diff_type="modified",
            attribute_diffs=[
                AttributeDiff("tags.team", "devops", "platform", "changed"),
            ],
        )
        assert diff.severity == DriftSeverity.LOW
        assert diff.category == DriftCategory.CONFIGURATION


# --- DriftDiffer tests ---


class TestDriftDiffer:
    def setup_method(self) -> None:
        """Set up test fixtures."""
        self.differ = DriftDiffer(store=None)

    def test_no_drift_when_equal(self) -> None:
        desired = {
            "res1": {"name": "test", "location": "uksouth"},
        }
        actual = {
            "res1": {"name": "test", "location": "uksouth"},
        }
        metadata = {
            "res1": {"type": "azurerm_resource_group", "id": "rg-1", "name": "test", "provider": "azure"},
        }
        result = self.differ.compare(desired, actual, metadata)
        assert len(result.resource_diffs) == 0
        assert result.drift_score == 100.0

    def test_detect_modified_attribute(self) -> None:
        desired = {"res1": {"name": "test", "location": "uksouth"}}
        actual = {"res1": {"name": "test", "location": "westus"}}
        metadata = {"res1": {"type": "azurerm_resource_group", "id": "rg-1", "name": "test", "provider": "azure"}}
        result = self.differ.compare(desired, actual, metadata)
        assert len(result.resource_diffs) == 1
        diff = result.resource_diffs[0]
        assert diff.diff_type == "modified"
        assert len(diff.attribute_diffs) == 1
        assert diff.attribute_diffs[0].attribute == "location"
        assert diff.attribute_diffs[0].desired == "uksouth"
        assert diff.attribute_diffs[0].actual == "westus"

    def test_detect_deleted_resource(self) -> None:
        desired = {"res1": {"name": "test"}}
        actual = {}
        metadata = {"res1": {"type": "aws_instance", "id": "i-1", "name": "test", "provider": "aws"}}
        result = self.differ.compare(desired, actual, metadata)
        assert len(result.resource_diffs) == 1
        assert result.resource_diffs[0].diff_type == "deleted"
        assert result.deleted_count == 1

    def test_detect_unmanaged_resource(self) -> None:
        desired = {}
        actual = {"extra": {"name": "rogue-vm"}}
        metadata = {"extra": {"type": "aws_instance", "id": "i-999", "name": "rogue-vm", "provider": "aws"}}
        result = self.differ.compare(desired, actual, metadata, provider="aws")
        assert len(result.resource_diffs) == 1
        assert result.resource_diffs[0].diff_type == "unmanaged"
        assert result.unmanaged_count == 1

    def test_multiple_drifts(self) -> None:
        desired = {
            "res1": {"name": "web", "size": "small"},
            "res2": {"name": "db", "encrypted": True},
        }
        actual = {
            "res1": {"name": "web", "size": "xlarge"},
            "res2": {"name": "db", "encrypted": False},
            "res3": {"name": "rogue"},
        }
        metadata = {
            "res1": {"type": "aws_instance", "id": "i-1", "name": "web", "provider": "aws"},
            "res2": {"type": "aws_db_instance", "id": "db-1", "name": "db", "provider": "aws"},
            "res3": {"type": "aws_instance", "id": "i-9", "name": "rogue", "provider": "aws"},
        }
        result = self.differ.compare(desired, actual, metadata, provider="aws")
        assert result.drifted_count == 2
        assert result.unmanaged_count == 1
        assert result.total_resources == 2
        assert result.drift_score < 100.0

    def test_skip_volatile_attributes(self) -> None:
        """Volatile attributes (id, etag, etc.) should be ignored."""
        desired = {"res1": {"name": "test", "etag": "abc123"}}
        actual = {"res1": {"name": "test", "etag": "xyz789"}}
        metadata = {"res1": {"type": "test", "id": "1", "name": "test", "provider": "test"}}
        result = self.differ.compare(desired, actual, metadata)
        # etag is volatile — should be skipped
        assert len(result.resource_diffs) == 0

    def test_added_attribute_in_actual(self) -> None:
        desired = {"res1": {"name": "test"}}
        actual = {"res1": {"name": "test", "new_attr": "surprise"}}
        metadata = {"res1": {"type": "test", "id": "1", "name": "test", "provider": "test"}}
        result = self.differ.compare(desired, actual, metadata)
        assert len(result.resource_diffs) == 1
        assert result.resource_diffs[0].attribute_diffs[0].diff_type == "added"

    def test_removed_attribute_from_actual(self) -> None:
        desired = {"res1": {"name": "test", "important": "yes"}}
        actual = {"res1": {"name": "test"}}
        metadata = {"res1": {"type": "test", "id": "1", "name": "test", "provider": "test"}}
        result = self.differ.compare(desired, actual, metadata)
        assert len(result.resource_diffs) == 1
        assert result.resource_diffs[0].attribute_diffs[0].diff_type == "removed"

    def test_scan_id_generated(self) -> None:
        result = self.differ.compare({}, {}, {})
        assert result.scan_id.startswith("scan-")
        assert len(result.scan_id) > 10

    def test_drift_score_perfect(self) -> None:
        desired = {"r1": {"a": "1"}, "r2": {"b": "2"}}
        actual = {"r1": {"a": "1"}, "r2": {"b": "2"}}
        meta = {
            "r1": {"type": "t", "id": "1", "name": "r1", "provider": "p"},
            "r2": {"type": "t", "id": "2", "name": "r2", "provider": "p"},
        }
        result = self.differ.compare(desired, actual, meta)
        assert result.drift_score == 100.0

    def test_case_insensitive_comparison(self) -> None:
        desired = {"res1": {"location": "UKSouth"}}
        actual = {"res1": {"location": "uksouth"}}
        metadata = {"res1": {"type": "test", "id": "1", "name": "test", "provider": "test"}}
        result = self.differ.compare(desired, actual, metadata)
        assert len(result.resource_diffs) == 0

    def test_persistence_to_store(self, tmp_path) -> None:
        """Test that results are persisted to DriftStore."""
        db_path = tmp_path / "test.db"
        store = DriftStore(db_path)
        differ = DriftDiffer(store=store)
        desired = {"res1": {"name": "test"}}
        actual = {"res1": {"name": "changed"}}
        metadata = {"res1": {"type": "t", "id": "1", "name": "test", "provider": "p"}}
        result = differ.compare(desired, actual, metadata)
        # Verify persistence
        drifts = store.get_drifts(scan_id=result.scan_id)
        assert len(drifts) > 0
        scans = store.get_scans()
        assert len(scans) == 1
        assert scans[0]["id"] == result.scan_id


# --- DriftResult tests ---


class TestDriftResult:
    def test_summary(self) -> None:
        result = DriftResult(
            scan_id="scan-test",
            provider="azure",
            iac_source="terraform",
            state_path="test.tfstate",
            total_resources=10,
            started_at=1000.0,
            completed_at=1002.5,
        )
        result.resource_diffs.append(ResourceDiff(
            resource_address="r1", resource_type="t1", resource_id="id1",
            resource_name="n1", provider="azure", diff_type="modified",
            attribute_diffs=[AttributeDiff("a", "b", "c", "changed")],
        ))
        summary = result.to_summary()
        assert summary["scan_id"] == "scan-test"
        assert summary["total_resources"] == 10
        assert summary["drifted"] == 1
        assert summary["duration_seconds"] == 2.5

    def test_has_critical(self) -> None:
        result = DriftResult(scan_id="s", provider="p", iac_source="i", state_path="s", total_resources=1)
        result.resource_diffs.append(ResourceDiff(
            resource_address="r", resource_type="t", resource_id="id",
            resource_name="n", provider="p", diff_type="modified",
            attribute_diffs=[AttributeDiff("public_access", False, True, "changed")],
        ))
        assert result.has_critical is True
