# By: Md. Fahim Bin Amin
# This file contains unit tests for validation functions in component_validator.py.
# These tests cover service, policy, routine, business service, team, and integration validation logic.

"""
Unit Tests for Component Validation Functions

This module validates the following functions from `component_validator.py`:

- validate_service_data
- validate_policy_data
- validate_routine_data
- validate_routine_rotations_data
- validate_business_service_data
- validate_team_data
- validate_integration_details
- service_circular_dependency

All external dependencies (e.g., string_validator, times, errors, var_names, permissions)
are mocked to isolate test logic and avoid reliance on external state or systems.

Test data is dynamically generated using FactoryBoy and Faker for robustness, randomness, and reproducibility.
"""

# ──────────────────────────────────────────────────────────────
# Imports
# ──────────────────────────────────────────────────────────────

from tests.fixtures.component_fixtures import ServiceFactory, TeamFactory, IntegrationDetailsFactory
from faker import Faker
from unittest.mock import patch
import pytest
import datetime

# Import functions under test
from validations.component_validator import (
    validate_service_data,
    validate_policy_data,
    validate_routine_data,
    validate_routine_rotations_data,
    validate_business_service_data,
    validate_team_data,
    validate_integration_details,
    service_circular_dependency
)

# ──────────────────────────────────────────────────────────────
# Faker Configuration
# ──────────────────────────────────────────────────────────────

fake = Faker()
Faker.seed(101)  # Ensures reproducible test data generation (high entropy, reproducible)

# ──────────────────────────────────────────────────────────────
# 🧪 Unit Test Function Index (Table of Contents)
# ──────────────────────────────────────────────────────────────

# ✅ test_validate_service_data_valid                               → Passes with valid service data
# ❌ test_validate_service_data_end_before_start                    → Fails if support_end is before support_start
# ❌ test_validate_service_data_invalid_time_format                 → Fails with unparseable time strings
# ❌ test_validate_service_data_invalid_timezone                    → Fails if timezone string is invalid
# ❌ test_validate_service_data_no_support_days                     → Fails if support_days is empty 
# ❌ test_validate_service_data_negative_retrigger                  → Fails if re_trigger_minutes is negative
# ✅ test_validate_service_data_missing_service_name                → Passes with None as service name
# ❌ test_validate_service_data_wrong_type_booleans                 → Fails if a boolean field is not a bool
# ❌ test_validate_service_data_wrong_type_integer                  → Fails if integer field is a string

# ✅ test_validate_policy_data_valid                                → Passes with correct policy levels and routines
# ❌ test_validate_policy_data_invalid_timestamp                    → Fails if timestamp is not a datetime
# ❌ test_validate_policy_data_empty_levels                         → Fails when policy_levels is empty
# ❌ test_validate_policy_data_missing_key                          → Fails if a policy level dict is missing keys
# ❌ test_validate_policy_data_invalid_routine_reference            → Fails if routine ID not in org_routines

# ✅ test_validate_routine_data_valid                               → Passes for a complete valid routine structure
# ❌ test_validate_routine_data_invalid_timezone                    → Fails for unrecognized timezone
# ❌ test_validate_routine_data_unknown_user                        → Fails if a user in rotations isn't assignable
# ❌ test_validate_routine_data_permission_denied                   → Fails if user lacks inclusion permission

# ✅ test_validate_routine_rotations_valid                          → Passes when all users have permission and exist
# ❌ test_validate_routine_rotations_not_a_list                     → Fails if a rotation entry is not a list
# ❌ test_validate_routine_rotations_unknown_user                   → Fails if a user is missing from assignable_users
# ❌ test_validate_routine_rotations_permission_denied              → Fails if a user lacks permission

# ✅ test_validate_business_service_data_valid                      → Passes for valid business service structure
# ❌ test_validate_business_service_data_invalid_timestamp          → Fails if timestamp is invalid type
# ❌ test_validate_business_service_data_no_supporting_services     → Fails if no services are provided
# ❌ test_validate_business_service_data_invalid_tech_ref           → Fails if tech service ID is not in ref list
# ❌ test_validate_business_service_data_invalid_biz_ref            → Fails for unknown business service ID
# ❌ test_validate_business_service_data_invalid_team_ref           → Fails for unknown team ID
# ❌ test_validate_business_service_data_invalid_urgency            → Fails if urgency not in allowed list
# ❌ test_validate_business_service_data_sql_injection_description  → Fails SQL injection validation

# ✅ test_validate_team_data_valid                                  → Passes with proper team members and roles
# ❌ test_validate_team_data_empty_users                            → Fails if no team members are given
# ❌ test_validate_team_data_duplicate_users                        → Fails if user appears more than once
# ❌ test_validate_team_data_invalid_role                           → Fails if a role is not in allowed roles

# ✅ test_validate_integration_details_valid                        → Passes for a valid webhook/email config
# ❌ test_validate_integration_details_invalid_type                 → Fails if type not in allowed_integration_types
# ❌ test_validate_integration_details_invalid_events               → Fails for disallowed event types

# ✅ test_service_circular_dependency_detected                      → Detects a circular dependency and returns loop point
# ✅ test_service_circular_dependency_not_found                     → Returns None if no loop exists
# ✅ test_service_circular_dependency_missing_start                 → Returns None if starting node not in service_map


# ──────────────────────────────────────────────────────────────
# Dummy Classes to Mock External Dependencies
# ──────────────────────────────────────────────────────────────

class DummyErrors:
    """
    Mock class to simulate error message constants used in component_validator.py.
    Used to avoid importing the real error module and isolate test dependencies.
    """
    err_time_end_before_start = "The end time must be after start time"
    err_internal_time_format = "Invalid time format"
    err_unknown_resource = "Unknown user"
    err_component_inclusion = "Permission denied"
    err_support_days_empty = "Support days cannot be empty"
    err_invalid_retrigger_minutes = "Invalid value: re_trigger_minutes must be >= 0"


class DummyStringValidator:
    """
    Mock implementation of string_validator functions to bypass real validations.

    Each method always returns True to simulate a permissive validator,
    allowing focus on logic unrelated to actual string format correctness.
    """
    @staticmethod
    def is_standard_name(name): return True

    @staticmethod
    def is_not_sql_injection(text): return True

    @staticmethod
    def is_date(date_str): return True

    @staticmethod
    def is_valid_preferred_username(name): return True

    @staticmethod
    def is_email_address(email): return True

    @staticmethod
    def is_web_url(url): return True


class DummyVarNames:
    """
    Mock class for variable name constants used in JSON or dict field parsing
    within component validation logic.
    This allows tests to work independently of the actual var_names module.
    """
    layer = "layer"
    layer_name = "layer_name"
    valid_start = "valid_start"
    valid_end = "valid_end"
    is_exception = "is_exception"
    rotation_start = "rotation_start"
    shift_length = "shift_length"
    rotation_frequency = "rotation_frequency"
    skip_days = "skip_days"
    rotations = "rotations"


class DummyTimes:
    """
    Mocked version of the times utility module.

    - `get_time_from_string`: Always returns 9:00 AM
    - `get_date_from_string`: Returns a specific date for known inputs or defaults to Jan 1, 2025
    """
    @staticmethod
    def get_time_from_string(time_str):
        # Simulate successful parsing of time strings
        return datetime.time(9, 0)

    @staticmethod
    def get_date_from_string(date_str):
        # Simulate successful parsing of specific date strings
        if date_str == "2025-05-01":
            return datetime.date(2025, 5, 1)
        elif date_str == "2025-06-01":
            return datetime.date(2025, 6, 1)
        # Default fallback for any other date string
        return datetime.date(2025, 1, 1)


class DummyPermissions:
    """
    Mock class to simulate the permissions module used in component_validator.py.

    - Always grants permission regardless of user role or required permission.
    - Used in unit tests to isolate logic from actual RBAC (Role-Based Access Control) checks.
    """
    # Constant simulating the permission key used to validate component inclusion
    USER_COMPONENTS_INCLUSION_PERMISSION = "components_inclusion"

    @staticmethod
    def has_user_permission(user_perm, required_perm):
        """
        Mock permission check that always returns True.

        Args:
            user_perm (str): The user's assigned permission.
            required_perm (str): The permission required to access a feature.

        Returns:
            bool: Always True for testing purposes.
        """
        return True


# ───────────────────────────────────────────────
# ✅ validate_service_data Test Cases
# ───────────────────────────────────────────────

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_valid(mock_time_parser):
    """
    ✅ Test Case: Valid service configuration should pass all validations.

    Mocks time parsing to simulate valid 9AM–5PM window.
    Uses Faker for random but realistic service data.
    """
    mock_time_parser.side_effect = [datetime.time(9, 0), datetime.time(17, 0)]
    service = ServiceFactory()

    validate_service_data(
        timestamp=datetime.datetime.now(),
        org_id=323,
        service_name=fake.bs().title(),                     # fake business-style service name
        description=fake.paragraph(nb_sentences=1),         # realistic fake description
        re_trigger_minutes=15,
        support_days=[0, 1, 2, 3, 4],                        # Monday to Friday
        support_start="09:00",
        support_end="17:00",
        support_timezone="UTC",
        de_prioritize=True,
        re_prioritize=False,
        allow_grouping=True
    )

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_end_before_start(mock_time_parser):
    """
    ❌ Test Case: Support end time is before start time.
    Should raise an AssertionError due to invalid logic.

    Mocks parsed times: 18:00 start and 09:00 end — triggers failure.
    """
    mock_time_parser.side_effect = [datetime.time(18, 0), datetime.time(9, 0)]

    with pytest.raises(AssertionError, match=DummyErrors.err_internal_time_format):
        validate_service_data(
            timestamp=datetime.datetime.now(),
            org_id=111,
            service_name=fake.bs().title(),
            description=fake.paragraph(nb_sentences=1),
            re_trigger_minutes=10,
            support_days=[0, 1],
            support_start="18:00",
            support_end="09:00",
            support_timezone="UTC",
            de_prioritize=True,
            re_prioritize=False,
            allow_grouping=True
        )

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_invalid_time_format(mock_time_parser):
    """
    ❌ Test Case: Time strings fail to parse into datetime.time objects.
    Should raise an AssertionError due to invalid format.

    Side effect simulates ValueError raised by internal time parser.
    """
    mock_time_parser.side_effect = ValueError("bad format")

    with pytest.raises(AssertionError, match=DummyErrors.err_internal_time_format):
        validate_service_data(
            timestamp=datetime.datetime.now(),
            org_id=1,
            service_name=fake.bs().title(),
            description=fake.paragraph(nb_sentences=1),
            re_trigger_minutes=10,
            support_days=[0, 1],
            support_start="bla bla",               # invalid string
            support_end="black sheep",             # invalid string
            support_timezone="UTC",
            de_prioritize=True,
            re_prioritize=False,
            allow_grouping=True
        )

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_invalid_timezone(mock_time_parser):
    """
    ❌ Test Case: Invalid timezone string should cause assertion failure.
    
    Mocks valid support time range, but sets an invalid timezone string.
    Expects AssertionError since timezone is not resolvable.
    """
    mock_time_parser.side_effect = [datetime.time(9, 0), datetime.time(17, 0)]

    with pytest.raises(AssertionError):
        validate_service_data(
            timestamp=datetime.datetime.now(),
            org_id=1,
            service_name=fake.bs(),
            description=fake.text(),
            re_trigger_minutes=10,
            support_days=[0, 1],
            support_start="09:00",
            support_end="17:00",
            support_timezone="NotARealTZ",  # invalid timezone
            de_prioritize=True,
            re_prioritize=False,
            allow_grouping=True
        )

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_no_support_days(mock_time_parser):
    """
    ❌ Test Case: support_days is empty.
    Should raise an AssertionError or be handled gracefully depending on logic.
    """
    mock_time_parser.side_effect = [datetime.time(9, 0), datetime.time(17, 0)]

    with pytest.raises(AssertionError, match=DummyErrors.err_support_days_empty):
        validate_service_data(
            timestamp=datetime.datetime.now(),
            org_id=999,
            service_name=fake.bs().title(),
            description=fake.paragraph(nb_sentences=1),
            re_trigger_minutes=10,
            support_days=[],  # empty list
            support_start="09:00",
            support_end="17:00",
            support_timezone="UTC",
            de_prioritize=False,
            re_prioritize=False,
            allow_grouping=False
        )

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_negative_retrigger(mock_time_parser):
    """
    ❌ Test Case: re_trigger_minutes is negative.
    Should raise an error.
    """
    mock_time_parser.side_effect = [datetime.time(9, 0), datetime.time(17, 0)]

    with pytest.raises(AssertionError, match=DummyErrors.err_invalid_retrigger_minutes):
        validate_service_data(
            timestamp=datetime.datetime.now(),
            org_id=123,
            service_name=fake.bs().title(),
            description=fake.paragraph(nb_sentences=1),
            re_trigger_minutes=-5,  # Invalid negative value
            support_days=[0, 1],
            support_start="09:00",
            support_end="17:00",
            support_timezone="UTC",
            de_prioritize=False,
            re_prioritize=True,
            allow_grouping=False
        )

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_missing_service_name(mock_time_parser):
    """
    ✅ Test Case: Service name is None (optional field).

    Should pass if validation logic allows service_name to be null.
    Uses mocked valid support time.
    """
    mock_time_parser.side_effect = [datetime.time(9, 0), datetime.time(17, 0)]

    validate_service_data(
        timestamp=datetime.datetime.now(),
        org_id=1,
        service_name=None,  # testing None input
        description=fake.text(),
        re_trigger_minutes=10,
        support_days=[0, 1],
        support_start="09:00",
        support_end="17:00",
        support_timezone="UTC",
        de_prioritize=True,
        re_prioritize=False,
        allow_grouping=True
    )

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_wrong_type_booleans(mock_time_parser):
    """
    ❌ Test Case: Boolean field has incorrect string type.

    `de_prioritize` is provided as "yes" (string), not a Python bool.
    Should raise AssertionError during type validation.
    """
    mock_time_parser.side_effect = [datetime.time(9, 0), datetime.time(17, 0)]

    with pytest.raises(AssertionError):
        validate_service_data(
            timestamp=datetime.datetime.now(),
            org_id=1,
            service_name=fake.bs(),
            description=fake.text(),
            re_trigger_minutes=10,
            support_days=[0, 1],
            support_start="09:00",
            support_end="17:00",
            support_timezone="UTC",
            de_prioritize="yes",  # ❌ invalid type
            re_prioritize=False,
            allow_grouping=True
        )

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times.get_time_from_string")
def test_validate_service_data_wrong_type_integer(mock_time_parser):
    """
    ❌ Test Case: Integer field `re_trigger_minutes` has string value.

    Should raise AssertionError because a string cannot be validated as an int.
    """
    mock_time_parser.side_effect = [datetime.time(9, 0), datetime.time(17, 0)]

    with pytest.raises(AssertionError):
        validate_service_data(
            timestamp=datetime.datetime.now(),
            org_id=1,
            service_name=fake.bs().title(),
            description=fake.paragraph(nb_sentences=1),
            re_trigger_minutes="ten",  # ❌ should be int
            support_days=[0, 1],
            support_start="09:00",
            support_end="17:00",
            support_timezone="UTC",
            de_prioritize=True,
            re_prioritize=False,
            allow_grouping=True
        )
# ───────────────────────────────────────────────
# ✅ validate_policy_data Test Cases
# ───────────────────────────────────────────────

def test_validate_policy_data_valid():
    """
    ✅ Test Case: All fields are valid and routines match references.

    Ensures `validate_policy_data` passes when policy name, levels, and org routines are all valid.
    """
    timestamp = datetime.datetime.now()
    org_id = 101
    policy_name = fake.catch_phrase()  # realistic policy name
    policy_levels = [
        {
            "assignee_level": 1,
            "level_minutes": 30,
            "routines": ["rtn-abc", "rtn-def"]
        }
    ]
    org_routines = [
        ["Routine A", "rtn-abc"],
        ["Routine B", "rtn-def"]
    ]

    validate_policy_data(timestamp, org_id, policy_name, policy_levels, org_routines)

def test_validate_policy_data_invalid_timestamp():
    """
    ❌ Test Case: Timestamp is not a datetime object.

    This test checks if non-datetime input for `timestamp` triggers validation error.
    """
    with pytest.raises(AssertionError):
        validate_policy_data(
            "not-a-datetime",  # ❌ invalid timestamp type
            101,
            "Invalid",
            [{"assignee_level": 1, "level_minutes": 10, "routines": ["rtn-abc"]}],
            [["Routine A", "rtn-abc"]]
        )

def test_validate_policy_data_empty_levels():
    """
    ❌ Test Case: policy_levels list is empty.

    Should raise AssertionError since at least one policy level is required.
    """
    with pytest.raises(AssertionError):
        validate_policy_data(
            datetime.datetime.now(),
            101,
            fake.catch_phrase(),
            [],  # ❌ empty list of levels
            [["Routine A", "rtn-abc"]]
        )

def test_validate_policy_data_missing_key():
    """
    ❌ Test Case: A policy level dictionary is missing required keys.

    Here, the "routines" key is omitted, so validation should fail.
    """
    bad_levels = [
        {
            "assignee_level": 1,
            "level_minutes": 10
            # ❌ missing "routines" key
        }
    ]
    with pytest.raises(AssertionError):
        validate_policy_data(
            datetime.datetime.now(),
            101,
            fake.catch_phrase(),
            bad_levels,
            [["Routine A", "rtn-abc"]]
        )

def test_validate_policy_data_invalid_routine_reference():
    """
    ❌ Test Case: A policy level refers to a routine ID not found in org_routines.

    Should raise AssertionError because "rtn-xxx" is undefined in org_routines.
    """
    bad_levels = [
        {
            "assignee_level": 1,
            "level_minutes": 10,
            "routines": ["rtn-xxx"]  # ❌ not in org_routines
        }
    ]
    with pytest.raises(AssertionError):
        validate_policy_data(
            datetime.datetime.now(),
            101,
            fake.catch_phrase(),
            bad_levels,
            [["Routine A", "rtn-abc"]]  # "rtn-xxx" not included here
        )

# ───────────────────────────────────────────────
# ✅ validate_routine_data Test Cases
# ───────────────────────────────────────────────

@pytest.fixture
def valid_routine_data():
    """
    Fixture: Returns a valid routine dictionary for testing.

    Includes:
    - One routine with proper timezone, date format, and schedule.
    - Valid assignable users mapped to appropriate roles.
    - Each rotation references known users.
    """
    user1, user2, user3 = fake.user_name(), fake.user_name(), fake.user_name()
    return {
        "timestamp": datetime.datetime.now(),
        "organization_id": 101,
        "routine_name": "Valid Routine",
        "routine_timezone": "UTC",
        "routine_layers": [{
            "layer": 1,
            "layer_name": "Primary Layer",
            "valid_start": "2025-05-01",
            "valid_end": "2025-06-01",
            "is_exception": False,
            "rotation_start": "09:00",
            "shift_length": "08:00",
            "rotation_frequency": 7,
            "skip_days": [5, 6],
            "rotations": [[user1], [user2], [user3]]
        }],
        "assignable_users": {
            user1: [1, "admin"],
            user2: [2, "admin"],
            user3: [3, "admin"]
        }
    }

# ──────────────────────────────────────────────────────────────
# ✅ Test: Valid routine data should pass without error
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times", new=DummyTimes)
@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.permissions", new=DummyPermissions)
@patch("validations.component_validator.var_names", new=DummyVarNames)
def test_validate_routine_data_valid(valid_routine_data):
    """
    ✅ Test Case: Routine data is fully valid.
    
    Confirms that a valid routine passes all checks without raising exceptions.
    """
    validate_routine_data(**valid_routine_data)

# ──────────────────────────────────────────────────────────────
# ❌ Test: Invalid timezone should raise AssertionError
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times", new=DummyTimes)
@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.permissions", new=DummyPermissions)
@patch("validations.component_validator.var_names", new=DummyVarNames)
def test_validate_routine_data_invalid_timezone(valid_routine_data):
    """
    ❌ Test Case: Timezone string is invalid and unrecognized.

    This should raise an AssertionError during validation.
    """
    valid_routine_data["routine_timezone"] = "Invalid/Timezone"  # ❌ Not a real timezone
    with pytest.raises(AssertionError):
        validate_routine_data(**valid_routine_data)

# ──────────────────────────────────────────────────────────────
# ❌ Test: Rotation contains unknown user not in assignable_users
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times", new=DummyTimes)
@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.permissions", new=DummyPermissions)
@patch("validations.component_validator.var_names", new=DummyVarNames)
def test_validate_routine_data_unknown_user(valid_routine_data):
    """
    ❌ Test Case: Rotation includes a user not listed in assignable_users.

    Should raise an AssertionError since unknown usernames can't be validated.
    """
    # Replace a known user in the rotation with an unknown one
    valid_routine_data["routine_layers"][0]["rotations"] = [[fake.user_name()]]
    
    with pytest.raises(AssertionError):
        validate_routine_data(**valid_routine_data)

# ──────────────────────────────────────────────────────────────
# ❌ Test: User in rotation lacks permission to be included
# ──────────────────────────────────────────────────────────────

# Override DummyPermissions to simulate denial of permission
class NoPermissionPermissions(DummyPermissions):
    @staticmethod
    def has_user_permission(user_perm, required_perm):
        # Always deny permission regardless of role
        return False

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.times", new=DummyTimes)
@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.permissions", new=NoPermissionPermissions)
@patch("validations.component_validator.var_names", new=DummyVarNames)
def test_validate_routine_data_permission_denied(valid_routine_data):
    """
    ❌ Test Case: A user in the rotation does not have inclusion permission.

    This should raise a PermissionError with message indicating lack of permission.
    """
    with pytest.raises(PermissionError, match="Permission denied"):
        validate_routine_data(**valid_routine_data)

# ──────────────────────────────────────────────────────────────
# ✅ Test: All users are valid and have permission
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.permissions", new=DummyPermissions)
def test_validate_routine_rotations_valid():
    """
    ✅ Test Case: All rotation users are valid and included in assignable_users.

    This test ensures that valid users with permission pass without any exception.
    """
    user1 = fake.user_name()
    user2 = fake.user_name()
    user3 = fake.user_name()

    routine_rotations = [[user1], [user2, user3]]  # Valid nested lists
    assignable_users = {
        user1: [1, "admin"],
        user2: [2, "admin"],
        user3: [3, "admin"]
    }

    # Should run without exception
    validate_routine_rotations_data(routine_rotations, assignable_users)

# ──────────────────────────────────────────────────────────────
# ❌ Test: Rotation entry is not a list
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.permissions", new=DummyPermissions)
def test_validate_routine_rotations_not_a_list():
    """
    ❌ Test Case: One rotation is a string instead of a list of users.

    This should trigger an AssertionError since the structure is invalid.
    """
    user = fake.user_name()

    # Second entry is invalid (string instead of a list)
    routine_rotations = [[user], "this-is-not-a-list-because-this-is-a-string"]

    assignable_users = {
        user: [1, "admin"]
    }

    with pytest.raises(AssertionError):
        validate_routine_rotations_data(routine_rotations, assignable_users)


# ──────────────────────────────────────────────────────────────
# ❌ Test: Rotation contains an unknown user
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.permissions", new=DummyPermissions)
def test_validate_routine_rotations_unknown_user():
    """
    ❌ Test Case: Rotation includes a user not in assignable_users.
    Should raise AssertionError with unknown user error.
    """
    user = fake.user_name()
    routine_rotations = [[fake.user_name()], [fake.name()]]
    assignable_users = {
        user: [1, "admin"]
    }

    with pytest.raises(AssertionError, match="Unknown user"):
        validate_routine_rotations_data(routine_rotations, assignable_users)

# ──────────────────────────────────────────────────────────────
# ❌ Test: User in rotation does not have inclusion permission
# ──────────────────────────────────────────────────────────────

class NoPermissionPermissions(DummyPermissions):
    @staticmethod
    def has_user_permission(user_perm, required_perm):
        return False  # Simulate permission denial

@patch("validations.component_validator.errors", new=DummyErrors)
@patch("validations.component_validator.permissions", new=NoPermissionPermissions)
def test_validate_routine_rotations_permission_denied():
    """
    ❌ Test Case: User exists but lacks inclusion permission.
    Should raise PermissionError with permission denied error.
    """
    user1 = fake.user_name()
    routine_rotations = [[user1]]  # Single rotation with one user
    assignable_users = {
        user1: [1, "admin"]  # User exists but no permission to include
    }

    with pytest.raises(PermissionError, match="Permission denied"):
        validate_routine_rotations_data(routine_rotations, assignable_users)

# ──────────────────────────────────────────────────────────────
# ✅ Test: All data is valid
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_urgency_levels", new=["low", "medium", "high", "critical"])
def test_validate_business_service_data_valid():
    """
    ✅ Test Case: Valid input for business service.
    Should pass without raising any exceptions.
    """
    validate_business_service_data(
    timestamp=datetime.datetime.now(),
    organization_id=101,
    service_name=fake.bs().title(),  
    supporting_technical_services=["svc-api-1"],
    supporting_business_services=None,
    associated_teams=["team-1"],
    description=fake.paragraph(nb_sentences=200),
    min_urgency="medium",
    org_tech_service_ref_ids=["svc-api-1", "svc-api-2"],
    org_business_service_ref_ids=["biz-core"],
    org_team_ref_ids=["team-1", "team-2"]
    )

# ──────────────────────────────────────────────────────────────
# ❌ Test: Invalid timestamp type
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_urgency_levels", new=["low", "medium", "high", "critical"])
def test_validate_business_service_data_invalid_timestamp():
    """
    ❌ Test Case: Timestamp is not a datetime object.
    Should raise AssertionError.
    """
    with pytest.raises(AssertionError):
        validate_business_service_data(
            timestamp="not-a-datetime",
            organization_id=101,
            service_name=fake.bs(),
            supporting_technical_services=["svc-api-1"],
            supporting_business_services=None,
            associated_teams=["team-1"],
            description=fake.paragraph(),
            min_urgency="low",
            org_tech_service_ref_ids=["svc-api-1"],
            org_business_service_ref_ids=[],
            org_team_ref_ids=["team-1"]
        )

# ──────────────────────────────────────────────────────────────
# ❌ Test: No supporting services provided
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_urgency_levels", new=["low", "medium", "high", "critical"])
def test_validate_business_service_data_no_supporting_services():
    """
    ❌ Test Case: Neither technical nor business services provided.
    Should raise AssertionError.
    """
    with pytest.raises(AssertionError):
        validate_business_service_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            service_name=fake.bs(),
            supporting_technical_services=[],
            supporting_business_services=None,
            associated_teams=None,
            description=fake.paragraph(),
            min_urgency="medium",
            org_tech_service_ref_ids=["svc-api-1"],
            org_business_service_ref_ids=["biz-core"],
            org_team_ref_ids=["team-1"]
        )

# ──────────────────────────────────────────────────────────────
# ❌ Test: Invalid technical service reference
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_urgency_levels", new=["low", "medium", "high", "critical"])
def test_validate_business_service_data_invalid_tech_ref():
    """
    ❌ Test Case: Technical service ID is not in org_tech_service_ref_ids.
    Should raise AssertionError.
    """
    with pytest.raises(AssertionError):
        validate_business_service_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            service_name=fake.bs().title(),
            supporting_technical_services=["svc-unknown"],
            supporting_business_services=None,
            associated_teams=["team-1"],
            description=fake.paragraph(),
            min_urgency="medium",
            org_tech_service_ref_ids=["svc-api-1", "svc-api-2"],
            org_business_service_ref_ids=[],
            org_team_ref_ids=["team-1"]
        )

# ──────────────────────────────────────────────────────────────
# ❌ Test: Invalid business service reference
# ──────────────────────────────────────────────────────────────


@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_urgency_levels", new=["low", "medium", "high", "critical"])
def test_validate_business_service_data_invalid_biz_ref():
    """
    ❌ Test Case: Business service ID is not in org_business_service_ref_ids.
    Should raise AssertionError.
    """
    service_name = fake.bs().title()
    description = fake.paragraph(nb_sentences=2)
    invalid_biz_id = fake.slug()  # simulate a business ID that would not be in the list

    with pytest.raises(AssertionError):
        validate_business_service_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            service_name=service_name,
            supporting_technical_services=None,
            supporting_business_services=[invalid_biz_id],  # invalid ID
            associated_teams=None,
            description=description,
            min_urgency="medium",
            org_tech_service_ref_ids=[],
            org_business_service_ref_ids=["biz-core"],  # allowed list doesn't include the above
            org_team_ref_ids=[]
        )


# ──────────────────────────────────────────────────────────────
# ❌ Test: Invalid team reference
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_urgency_levels", new=["low", "medium", "high", "critical"])
def test_validate_business_service_data_invalid_team_ref():
    """
    ❌ Test Case: Team ID is not in org_team_ref_ids.
    Should raise AssertionError.
    """
    invalid_team_id = fake.slug()  # simulates a team ID not in org_team_ref_ids

    with pytest.raises(AssertionError):
        validate_business_service_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            service_name=fake.bs().title(),
            supporting_technical_services=["svc-api-1"],
            supporting_business_services=None,
            associated_teams=[invalid_team_id],
            description=fake.paragraph(nb_sentences=2),
            min_urgency="medium",
            org_tech_service_ref_ids=["svc-api-1"],
            org_business_service_ref_ids=[],
            org_team_ref_ids=["team-1"]  # allowed list doesn't include the above
        )


# ──────────────────────────────────────────────────────────────
# ❌ Test: Urgency level not in allowed list
# ──────────────────────────────────────────────────────────────

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_urgency_levels", new=["low", "medium", "high", "critical"])
def test_validate_business_service_data_invalid_urgency():
    """
    ❌ Test Case: Urgency level not in allowed_urgency_levels.
    Should raise AssertionError.
    """
    with pytest.raises(AssertionError):
        validate_business_service_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            service_name=fake.bs().title(),
            supporting_technical_services=["svc-api-1"],
            supporting_business_services=None,
            associated_teams=None,
            description=fake.paragraph(nb_sentences=1),
            min_urgency="urgent",  # invalid urgency
            org_tech_service_ref_ids=["svc-api-1"],
            org_business_service_ref_ids=[],
            org_team_ref_ids=[]
        )


# ──────────────────────────────────────────────────────────────
# ❌ Test: SQL-injection-like description
# ──────────────────────────────────────────────────────────────

class MaliciousStringValidator(DummyStringValidator):
    @staticmethod
    def is_not_sql_injection(text): return False

@patch("validations.component_validator.string_validator", new=MaliciousStringValidator)
@patch("validations.component_validator.configuration.allowed_urgency_levels", new=["low", "medium", "high", "critical"])
def test_validate_business_service_data_sql_injection_description():
    """
    ❌ Test Case: Description fails SQL injection check.
    Should raise AssertionError.
    """
    with pytest.raises(AssertionError):
        validate_business_service_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            service_name=fake.bs().title(),
            supporting_technical_services=["svc-api-1"],
            supporting_business_services=None,
            associated_teams=None,
            description="DROP TABLE {}; --".format(fake.word()),  # dynamic SQL-like injection
            min_urgency="low",
            org_tech_service_ref_ids=["svc-api-1"],
            org_business_service_ref_ids=[],
            org_team_ref_ids=[]
        )


@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.roles.advanced_component_roles", new=["admin", "member"])
def test_validate_team_data_valid():
    """
    ✅ Test Case: Valid team data should pass all assertions.
    """
    team_name = fake.bs().title()
    description = fake.paragraph(nb_sentences=2)
    user1 = fake.user_name()
    user2 = fake.user_name()

    validate_team_data(
        timestamp=datetime.datetime.now(),
        organization_id=101,
        team_name=team_name,
        team_users=[[user1, "admin"], [user2, "member"]],
        is_public=True,
        description=description
    )


@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.roles.advanced_component_roles", new=["admin", "member"])
def test_validate_team_data_empty_users():
    """
    ❌ Test Case: No team members should raise ValueError.
    """
    with pytest.raises(ValueError):
        validate_team_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            team_name=fake.bs(),
            team_users=[],
            is_public=False,
            description=None
        )

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.roles.advanced_component_roles", new=["admin", "member"])
def test_validate_team_data_duplicate_users():
    """
    ❌ Test Case: Duplicate user IDs should raise AssertionError.
    """
    user = fake.user_name()
    team_name = fake.bs().title()
    description = fake.paragraph(nb_sentences=1)

    with pytest.raises(AssertionError):
        validate_team_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            team_name=team_name,
            team_users=[[user, "admin"], [user, "member"]],  # duplicate user
            is_public=False,
            description=description
        )


@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.roles.advanced_component_roles", new=["admin", "member"])
def test_validate_team_data_invalid_role():
    """
    ❌ Test Case: Invalid team role should raise AssertionError.
    """
    team_name = fake.bs().title()
    username = fake.user_name()
    description = fake.paragraph(nb_sentences=1)

    with pytest.raises(AssertionError):
        validate_team_data(
            timestamp=datetime.datetime.now(),
            organization_id=101,
            team_name=team_name,
            team_users=[[username, "unknown_role"]],
            is_public=False,
            description=description
        )


@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_integration_types", new=["webhook", "email"])
@patch("validations.component_validator.configuration.allowed_integration_events", new=["incident.created", "incident.resolved"])
def test_validate_integration_details_valid():
    """
    ✅ Test Case: Valid integration data with optional fields should pass.
    """
    integ_type = "webhook"
    integ_name = f"{fake.domain_word().capitalize()} Integration"
    integ_url = fake.url()
    outgoing_events = ["incident.created"]
    payload_map = {"message": "incident.message"}
    public_access = True

    validate_integration_details(
        integ_type=integ_type,
        integ_name=integ_name,
        integ_url=integ_url,
        outgoing_events=outgoing_events,
        payload_map=payload_map,
        public_access=public_access
    )


@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_integration_types", new=["webhook", "email"])
def test_validate_integration_details_invalid_type():
    """
    ❌ Test Case: Integration type not allowed should raise AssertionError.
    """
    with pytest.raises(AssertionError):
        validate_integration_details(
            integ_type="ftp",  # invalid type
            integ_name=fake.bs().title()  # optional realism
        )

@patch("validations.component_validator.string_validator", new=DummyStringValidator)
@patch("validations.component_validator.configuration.allowed_integration_types", new=["webhook"])
@patch("validations.component_validator.configuration.allowed_integration_events", new=["incident.created"])
def test_validate_integration_details_invalid_events():
    """
    ❌ Test Case: Events not in allowed list should raise AssertionError.
    """
    with pytest.raises(AssertionError):
        validate_integration_details(
            integ_type="webhook",
            integ_name=fake.bs().title(),  # optional realism
            outgoing_events=["unknown.event"]  # invalid event
        )


def test_service_circular_dependency_detected():
    """
    ✅ Test Case: Circular dependency detected in service_map.
    Should return the name of the service where the loop is detected.
    """
    service_map = {
        "A": {"name": "Service A", "dependencies": ["B"]},
        "B": {"name": "Service B", "dependencies": ["C"]},
        "C": {"name": "Service C", "dependencies": ["A"]},  # loop back to A
    }
    result = service_circular_dependency("A", "A", service_map)
    assert result == "Service C"  # match actual circular return


def test_service_circular_dependency_not_found():
    """
    ✅ Test Case: No circular dependency should return None.
    """
    service_map = {
        "A": {"name": "Service A", "dependencies": ["B"]},
        "B": {"name": "Service B", "dependencies": ["C"]},
        "C": {"name": "Service C", "dependencies": []},
    }
    result = service_circular_dependency("A", "A", service_map)
    assert result is None

def test_service_circular_dependency_missing_start():
    """
    ✅ Test Case: 'start' not in service_map should return None.
    """
    service_map = {
        "X": {"name": "Service X", "dependencies": []},
    }
    result = service_circular_dependency("A", "A", service_map)
    assert result is None