"""
Comprehensive test suite for reward calculator module testing RewardCalculator class,
RewardCalculatorConfig, RewardResult, and TerminationResult data classes with mathematical
accuracy validation, performance benchmarking, edge case handling, API compliance
verification, and reproducibility testing for goal-based sparse reward system in
plume_nav_sim reinforcement learning environment.

This test module provides enterprise-grade validation with >95% code coverage targeting
sub-millisecond performance validation, numerical precision testing, and comprehensive
error condition coverage for production-ready reward calculation functionality.
"""

import math  # >=3.10 - Mathematical functions for precision testing and distance calculation validation
import time  # >=3.10 - Performance timing measurements for reward calculation latency validation
import warnings  # >=3.10 - Warning management for deprecation and performance warnings in tests

import numpy as np  # >=2.1.0 - Numerical testing, array operations, and mathematical validation for reward calculations

# Standard library imports with version comments
import pytest  # >=8.0.0 - Testing framework for fixtures, parametrized tests, and comprehensive test execution

# Internal imports for exception handling and validation
from plume_nav_sim._compat import ValidationError

# Internal imports for constants and mathematical operations
from plume_nav_sim.core.constants import (
    DEFAULT_GOAL_RADIUS,
    DISTANCE_PRECISION,
    PERFORMANCE_TARGET_STEP_LATENCY_MS,
    REWARD_DEFAULT,
    REWARD_GOAL_REACHED,
)

# Internal imports for reward calculator components under test
from plume_nav_sim.core.reward_calculator import (
    RewardCalculator,
    RewardCalculatorConfig,
    RewardResult,
    TerminationResult,
    create_reward_calculator,
    validate_reward_config,
)

# Internal imports for types and coordinate operations
from plume_nav_sim.core.types import (
    AgentState,
    Coordinates,
    calculate_euclidean_distance,
)

# Global test configuration constants
PERFORMANCE_TEST_ITERATIONS = 1000
DISTANCE_TOLERANCE = 1e-10
TIMING_TOLERANCE_MS = 0.1
CACHE_TEST_SIZE = 500
STRESS_TEST_ITERATIONS = 10000
BOUNDARY_TEST_COORDINATES = [(0, 0), (1000, 1000), (-1, -1)]


@pytest.mark.unit
def test_reward_calculator_initialization():
    """
    Test RewardCalculator initialization with various configurations ensuring proper setup,
    validation, and default parameter handling with component integration validation.
    """
    # Create basic RewardCalculatorConfig with default parameters
    config = RewardCalculatorConfig(
        goal_radius=DEFAULT_GOAL_RADIUS,
        reward_goal_reached=REWARD_GOAL_REACHED,
        reward_default=REWARD_DEFAULT,
    )

    # Initialize RewardCalculator with valid configuration
    calculator = RewardCalculator(config)

    # Verify all configuration parameters are properly stored
    assert calculator.config.goal_radius == DEFAULT_GOAL_RADIUS
    assert calculator.config.reward_goal_reached == REWARD_GOAL_REACHED
    assert calculator.config.reward_default == REWARD_DEFAULT
    assert calculator.config.enable_caching is True
    assert calculator.config.enable_performance_monitoring is True

    # Check performance metrics initialization if enabled
    assert isinstance(calculator.performance_metrics, dict)
    assert calculator.performance_metrics == {}

    # Validate distance cache initialization with proper size limits
    assert isinstance(calculator.distance_cache, dict)
    assert len(calculator.distance_cache) == 0

    # Test logger setup and component identification
    assert calculator.logger is not None
    assert hasattr(calculator, "logger")

    # Verify calculation counter initialization to zero
    assert calculator.calculation_count == 0
    assert calculator.goal_achievement_stats["total_calculations"] == 0
    assert calculator.goal_achievement_stats["goals_achieved"] == 0


@pytest.mark.unit
def test_reward_calculator_config_validation():
    """
    Test RewardCalculatorConfig validation with valid and invalid parameters ensuring
    comprehensive parameter checking and error reporting with edge case handling.
    """
    # Test valid configuration with standard parameters passes validation
    valid_config = RewardCalculatorConfig(
        goal_radius=5.0, reward_goal_reached=1.0, reward_default=0.0
    )
    assert valid_config.validate() is True

    # Test negative goal_radius raises ValidationError with proper context
    with pytest.raises(ValidationError) as exc_info:
        invalid_config = RewardCalculatorConfig(
            goal_radius=-1.0, reward_goal_reached=1.0, reward_default=0.0
        )
    assert "goal_radius must be non-negative" in str(exc_info.value)

    # Test invalid reward values (NaN, infinity) raise ValidationError
    with pytest.raises(ValidationError):
        invalid_config = RewardCalculatorConfig(
            goal_radius=5.0, reward_goal_reached=float("nan"), reward_default=0.0
        )

    # Test configuration with reward_goal_reached == reward_default fails validation
    with pytest.raises(ValidationError) as exc_info:
        invalid_config = RewardCalculatorConfig(
            goal_radius=5.0, reward_goal_reached=1.0, reward_default=1.0
        )
        invalid_config.validate(strict_mode=True)
    assert "reward_goal_reached must differ from reward_default" in str(exc_info.value)

    # Test extreme goal_radius values for mathematical consistency
    extreme_config = RewardCalculatorConfig(
        goal_radius=999.0, reward_goal_reached=1.0, reward_default=0.0
    )
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        assert extreme_config.validate() is True

    # Verify error messages contain specific parameter information
    with pytest.raises(ValidationError) as exc_info:
        RewardCalculatorConfig(
            goal_radius="invalid", reward_goal_reached=1.0, reward_default=0.0
        )
    assert "goal_radius" in str(exc_info.value)

    # Test strict validation mode with additional constraint checking
    strict_config = RewardCalculatorConfig(
        goal_radius=1e-15, reward_goal_reached=1.0, reward_default=0.0
    )
    with pytest.raises(ValidationError):
        strict_config.validate(strict_mode=True)


@pytest.mark.unit
@pytest.mark.parametrize(
    "agent_pos,source_pos,goal_radius,expected_reward",
    [
        (Coordinates(0, 0), Coordinates(0, 0), 0, REWARD_GOAL_REACHED),
        (Coordinates(0, 0), Coordinates(1, 1), 0, REWARD_DEFAULT),
        (Coordinates(5, 5), Coordinates(5, 6), 2, REWARD_GOAL_REACHED),
        (Coordinates(10, 10), Coordinates(12, 12), 3, REWARD_GOAL_REACHED),
        (Coordinates(0, 0), Coordinates(10, 0), 5, REWARD_DEFAULT),
    ],
)
def test_reward_calculation_accuracy(
    agent_pos, source_pos, goal_radius, expected_reward
):
    """
    Test reward calculation mathematical accuracy with various agent-source distance
    scenarios ensuring correct sparse reward implementation with comprehensive validation.
    """
    # Create reward calculator with specified goal_radius
    config = RewardCalculatorConfig(
        goal_radius=goal_radius,
        reward_goal_reached=REWARD_GOAL_REACHED,
        reward_default=REWARD_DEFAULT,
    )
    calculator = RewardCalculator(config)

    # Calculate reward for given agent and source positions
    result = calculator.calculate_reward(agent_pos, source_pos)

    # Verify reward value matches expected_reward exactly
    assert result.reward == expected_reward

    # Check goal_reached flag in RewardResult matches expectation
    expected_goal_reached = expected_reward == REWARD_GOAL_REACHED
    assert result.goal_reached == expected_goal_reached

    # Validate distance_to_goal calculation accuracy within DISTANCE_TOLERANCE
    expected_distance = calculate_euclidean_distance(agent_pos, source_pos)
    assert abs(result.distance_to_goal - expected_distance) < DISTANCE_TOLERANCE

    # Test edge cases at exact goal_radius boundary
    if goal_radius > 0:
        boundary_distance = result.distance_to_goal
        if abs(boundary_distance - goal_radius) < DISTANCE_PRECISION:
            # At exact boundary - should achieve goal
            assert result.goal_reached or boundary_distance <= goal_radius

    # Verify sparse reward structure with multiple non-goal positions
    if expected_reward == REWARD_DEFAULT:
        assert result.distance_to_goal > goal_radius


@pytest.mark.unit
def test_distance_calculation_precision():
    """
    Test Euclidean distance calculation precision and numerical stability with various
    coordinate combinations ensuring mathematical accuracy and edge case handling.
    """
    config = RewardCalculatorConfig(
        goal_radius=0, reward_goal_reached=1.0, reward_default=0.0
    )
    calculator = RewardCalculator(config)

    # Test distance calculation between identical coordinates equals 0.0
    identical_coords = Coordinates(5, 5)
    distance = calculator.get_distance_to_goal(identical_coords, identical_coords)
    assert distance == 0.0

    # Test distance calculation with simple integer coordinates (3,4,5 triangle)
    coord1 = Coordinates(0, 0)
    coord2 = Coordinates(3, 4)
    distance = calculator.get_distance_to_goal(coord1, coord2)
    assert abs(distance - 5.0) < DISTANCE_TOLERANCE

    # Verify distance calculation with large coordinate values maintains precision
    large_coord1 = Coordinates(1000000, 1000000)
    large_coord2 = Coordinates(1000003, 1000004)
    large_distance = calculator.get_distance_to_goal(large_coord1, large_coord2)
    assert abs(large_distance - 5.0) < DISTANCE_TOLERANCE

    # Test distance calculation with very close coordinates near precision limits
    close_coord1 = Coordinates(0, 0)
    close_coord2 = Coordinates(1, 0)
    close_distance = calculator.get_distance_to_goal(close_coord1, close_coord2)
    assert abs(close_distance - 1.0) < DISTANCE_TOLERANCE

    # Compare direct calculation with calculate_euclidean_distance function
    test_pos1 = Coordinates(7, 24)
    test_pos2 = Coordinates(0, 0)
    calc_distance = calculator.get_distance_to_goal(test_pos1, test_pos2)
    direct_distance = calculate_euclidean_distance(test_pos1, test_pos2)
    assert abs(calc_distance - direct_distance) < DISTANCE_TOLERANCE

    # Test symmetry: distance(A,B) == distance(B,A)
    distance_ab = calculator.get_distance_to_goal(test_pos1, test_pos2)
    distance_ba = calculator.get_distance_to_goal(test_pos2, test_pos1)
    assert abs(distance_ab - distance_ba) < DISTANCE_TOLERANCE

    # Validate high precision mode for critical calculations
    high_precision_distance = calculator.get_distance_to_goal(
        test_pos1, test_pos2, use_cache=False, high_precision=True
    )
    assert abs(high_precision_distance - calc_distance) < DISTANCE_TOLERANCE / 10


@pytest.mark.unit
@pytest.mark.parametrize("goal_radius", [0, 0.5, 1.0, 2.5, 5.0])
def test_goal_detection_boundary_conditions(goal_radius):
    """
    Test goal detection at boundary conditions with various goal radius values ensuring
    accurate termination logic with floating-point precision edge case handling.
    """
    # Create reward calculator with specified goal_radius
    config = RewardCalculatorConfig(
        goal_radius=goal_radius, reward_goal_reached=1.0, reward_default=0.0
    )
    calculator = RewardCalculator(config)

    source_pos = Coordinates(10, 10)

    # Test coordinates exactly at goal_radius distance return goal achieved
    if goal_radius > 0:
        # Place agent at exact goal_radius distance
        offset = goal_radius / math.sqrt(2)  # Equal x,y offset for exact distance
        boundary_pos = Coordinates(
            int(round(source_pos.x + offset)), int(round(source_pos.y + offset))
        )

        result = calculator.calculate_reward(boundary_pos, source_pos)
        actual_distance = calculate_euclidean_distance(boundary_pos, source_pos)

        # Goal should be achieved if distance <= goal_radius
        expected_goal_reached = actual_distance <= goal_radius
        assert result.goal_reached == expected_goal_reached

    # Test coordinates slightly inside goal_radius (goal_radius - DISTANCE_PRECISION/2)
    if goal_radius > DISTANCE_PRECISION:
        inside_distance = goal_radius - DISTANCE_PRECISION / 2
        inside_offset = inside_distance / math.sqrt(2)
        inside_pos = Coordinates(
            int(round(source_pos.x + inside_offset)),
            int(round(source_pos.y + inside_offset)),
        )

        result = calculator.calculate_reward(inside_pos, source_pos)
        # Should achieve goal when inside radius
        if calculate_euclidean_distance(inside_pos, source_pos) <= goal_radius:
            assert result.goal_reached is True

    # Test coordinates slightly outside goal_radius (goal_radius + DISTANCE_PRECISION/2)
    outside_distance = goal_radius + DISTANCE_PRECISION / 2
    outside_offset = outside_distance / math.sqrt(2)
    outside_pos = Coordinates(
        int(round(source_pos.x + outside_offset)),
        int(round(source_pos.y + outside_offset)),
    )

    result = calculator.calculate_reward(outside_pos, source_pos)
    actual_outside_distance = calculate_euclidean_distance(outside_pos, source_pos)

    # Should not achieve goal when outside radius
    if actual_outside_distance > goal_radius:
        assert result.goal_reached is False

    # Verify goal detection with goal_radius = 0 requires exact position match
    if goal_radius == 0:
        exact_match_result = calculator.calculate_reward(source_pos, source_pos)
        assert exact_match_result.goal_reached is True

        non_match_pos = Coordinates(source_pos.x + 1, source_pos.y)
        non_match_result = calculator.calculate_reward(non_match_pos, source_pos)
        assert non_match_result.goal_reached is False

    # Test floating-point precision edge cases at boundary
    precision_test_distance = goal_radius + 1e-15
    if precision_test_distance != goal_radius:  # Only if precision matters
        precision_offset = precision_test_distance / math.sqrt(2)
        precision_pos = Coordinates(
            int(round(source_pos.x + precision_offset)),
            int(round(source_pos.y + precision_offset)),
        )

        result = calculator.calculate_reward(precision_pos, source_pos)
        # Validate consistent goal detection with high precision
        actual_precision_distance = calculate_euclidean_distance(
            precision_pos, source_pos
        )
        expected_precision_goal = actual_precision_distance <= goal_radius
        assert result.goal_reached == expected_precision_goal

    # Validate consistent goal detection across multiple calculations
    test_pos = Coordinates(source_pos.x + 1, source_pos.y + 1)
    result1 = calculator.calculate_reward(test_pos, source_pos)
    result2 = calculator.calculate_reward(test_pos, source_pos)
    assert result1.goal_reached == result2.goal_reached
    assert abs(result1.distance_to_goal - result2.distance_to_goal) < DISTANCE_TOLERANCE


@pytest.mark.integration
def test_termination_logic():
    """
    Test episode termination logic including goal achievement and step limit truncation
    with comprehensive state analysis and Gymnasium API compliance validation.
    """
    config = RewardCalculatorConfig(
        goal_radius=2.0, reward_goal_reached=1.0, reward_default=0.0
    )
    calculator = RewardCalculator(config)

    source_pos = Coordinates(10, 10)
    max_steps = 100

    # Create agent state at source location with various step counts
    goal_agent_state = AgentState(
        position=source_pos, step_count=50, goal_reached=False
    )

    # Test termination when goal is reached returns terminated=True, truncated=False
    goal_termination = calculator.check_termination(
        goal_agent_state, source_pos, max_steps
    )
    assert goal_termination.terminated is True
    assert goal_termination.truncated is False
    assert "Goal achieved" in goal_termination.termination_reason

    # Test termination when step limit exceeded returns terminated=False, truncated=True
    limit_agent_state = AgentState(
        position=Coordinates(20, 20),  # Far from source
        step_count=max_steps,
        goal_reached=False,
    )

    limit_termination = calculator.check_termination(
        limit_agent_state, source_pos, max_steps
    )
    assert limit_termination.terminated is False
    assert limit_termination.truncated is True
    assert "Step limit reached" in limit_termination.termination_reason

    # Test termination when both goal reached and step limit exceeded prioritizes goal achievement
    both_agent_state = AgentState(
        position=source_pos,  # At source (goal achieved)
        step_count=max_steps,  # Step limit reached
        goal_reached=False,
    )

    both_termination = calculator.check_termination(
        both_agent_state, source_pos, max_steps
    )
    assert both_termination.terminated is True
    assert both_termination.truncated is False  # Goal achievement takes priority

    # Verify termination_reason provides clear explanation of episode end
    ongoing_agent_state = AgentState(
        position=Coordinates(15, 15),  # Not at goal
        step_count=50,  # Under step limit
        goal_reached=False,
    )

    ongoing_termination = calculator.check_termination(
        ongoing_agent_state, source_pos, max_steps
    )
    assert ongoing_termination.terminated is False
    assert ongoing_termination.truncated is False
    assert "Episode ongoing" in ongoing_termination.termination_reason

    # Test final state information includes distance and step count
    assert goal_termination.final_step_count == 50
    assert goal_termination.final_distance is not None
    assert goal_termination.final_distance >= 0.0

    # Validate TerminationResult contains comprehensive episode analysis
    assert hasattr(goal_termination, "termination_details")
    assert isinstance(goal_termination.termination_details, dict)
    assert "goal_radius" in goal_termination.termination_details
    assert "max_steps" in goal_termination.termination_details


@pytest.mark.integration
def test_agent_reward_integration():
    """
    Test reward calculator integration with AgentState for reward accumulation and
    state synchronization with comprehensive coordination validation.
    """
    config = RewardCalculatorConfig(
        goal_radius=1.0, reward_goal_reached=1.0, reward_default=0.0
    )
    calculator = RewardCalculator(config)

    # Create agent state with initial position and zero reward
    agent_state = AgentState(
        position=Coordinates(5, 5), step_count=0, goal_reached=False
    )
    initial_reward = agent_state.total_reward

    source_pos = Coordinates(5, 5)  # Same as agent position - goal achieved

    # Calculate reward using reward calculator
    reward_result = calculator.calculate_reward(agent_state.position, source_pos)
    assert reward_result.reward == 1.0
    assert reward_result.goal_reached is True

    # Update agent state with calculated reward using update_agent_reward
    calculator.update_agent_reward(agent_state, reward_result)

    # Verify agent total_reward reflects accumulated rewards correctly
    assert agent_state.total_reward == initial_reward + reward_result.reward

    # Test goal_reached flag synchronization between RewardResult and AgentState
    assert agent_state.goal_reached == reward_result.goal_reached

    # Validate agent state consistency after multiple reward updates
    second_reward_result = calculator.calculate_reward(
        Coordinates(10, 10),
        source_pos,  # Different position
    )
    calculator.update_agent_reward(
        agent_state, second_reward_result, update_goal_status=False
    )

    # Total reward should accumulate properly
    expected_total = initial_reward + reward_result.reward + second_reward_result.reward
    assert agent_state.total_reward == expected_total

    # Goal status should remain from first update since update_goal_status=False
    assert agent_state.goal_reached is True

    # Test error handling when agent state is inconsistent
    class InvalidAgentState:
        def __init__(self):
            pass  # Missing required methods

    invalid_agent = InvalidAgentState()
    with pytest.raises(ValidationError):
        calculator.update_agent_reward(invalid_agent, reward_result)


@pytest.mark.performance
@pytest.mark.slow
@pytest.mark.xfail(
    reason="Known performance regression; timing thresholds under review", strict=False
)
def test_performance_targets():
    """
    Test reward calculation performance meets <1ms latency targets with comprehensive
    timing validation and statistical analysis for optimization verification.
    """
    config = RewardCalculatorConfig(
        goal_radius=2.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        enable_performance_monitoring=True,
    )

    # Create reward calculator with performance monitoring enabled
    calculator = RewardCalculator(config)

    test_positions = [
        (Coordinates(0, 0), Coordinates(5, 5)),
        (Coordinates(10, 10), Coordinates(12, 12)),
        (Coordinates(100, 100), Coordinates(105, 103)),
    ]

    timing_results = []

    # Execute PERFORMANCE_TEST_ITERATIONS reward calculations with timing measurement
    for i in range(PERFORMANCE_TEST_ITERATIONS):
        agent_pos, source_pos = test_positions[i % len(test_positions)]

        # Record timing for each calculation using high-precision timing
        start_time = time.perf_counter()
        result = calculator.calculate_reward(agent_pos, source_pos)
        end_time = time.perf_counter()

        calculation_time_ms = (end_time - start_time) * 1000
        timing_results.append(calculation_time_ms)

        # Verify each result is valid
        assert isinstance(result, RewardResult)
        assert isinstance(result.reward, (int, float))
        assert isinstance(result.goal_reached, bool)

    # Calculate average, median, and maximum execution times
    timing_array = np.array(timing_results)
    average_time = np.mean(timing_array)
    median_time = np.median(timing_array)
    max_time = np.max(timing_array)
    percentile_95 = np.percentile(timing_array, 95)

    # Verify average calculation time remains close to configured target
    target_time = PERFORMANCE_TARGET_STEP_LATENCY_MS * 1.5
    assert (
        average_time < target_time
    ), f"Average time {average_time:.3f}ms exceeds target {target_time:.3f}ms"

    # Ensure 95th percentile execution time meets performance targets
    assert (
        percentile_95 < PERFORMANCE_TARGET_STEP_LATENCY_MS * 2.0
    ), f"95th percentile {percentile_95:.3f}ms exceeds target {(PERFORMANCE_TARGET_STEP_LATENCY_MS * 2.0):.3f}ms"

    # Test performance with various coordinate ranges and goal radius values
    large_coord_times = []
    for i in range(100):
        large_agent = Coordinates(i * 100, i * 100)
        large_source = Coordinates((i + 1) * 100, (i + 1) * 100)

        start_time = time.perf_counter()
        calculator.calculate_reward(large_agent, large_source)
        end_time = time.perf_counter()

        large_coord_times.append((end_time - start_time) * 1000)

    large_coord_average = np.mean(large_coord_times)
    assert (
        large_coord_average < PERFORMANCE_TARGET_STEP_LATENCY_MS * 1.5
    ), "Large coordinate performance degradation detected"

    # Generate performance report with optimization recommendations
    _ = {
        "average_time_ms": average_time,
        "median_time_ms": median_time,
        "max_time_ms": max_time,
        "percentile_95_ms": percentile_95,
        "target_compliance": average_time < PERFORMANCE_TARGET_STEP_LATENCY_MS * 1.5,
        "total_calculations": PERFORMANCE_TEST_ITERATIONS,
        "large_coordinate_average_ms": large_coord_average,
    }

    # Verify performance metrics collection
    stats = calculator.get_reward_statistics(include_performance_analysis=True)
    assert "performance_analysis" in stats
    assert stats["total_calculations"] >= PERFORMANCE_TEST_ITERATIONS


@pytest.mark.performance
def test_caching_efficiency():
    """
    Test distance calculation caching for performance optimization with cache hit/miss
    analysis and memory management validation for computational efficiency.
    """
    config = RewardCalculatorConfig(
        goal_radius=1.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        enable_caching=True,
    )

    # Create reward calculator with caching enabled
    calculator = RewardCalculator(config)

    test_agent = Coordinates(5, 5)
    test_source = Coordinates(10, 10)

    # Perform repeated distance calculations with identical coordinates
    first_time = time.perf_counter()
    first_result = calculator.calculate_reward(test_agent, test_source)
    _ = time.perf_counter() - first_time

    # Second calculation should be faster due to caching
    second_time = time.perf_counter()
    second_result = calculator.calculate_reward(test_agent, test_source)
    _ = time.perf_counter() - second_time

    # Verify cache hit improves subsequent calculation performance
    assert len(calculator.distance_cache) > 0, "Distance cache should contain entries"
    assert first_result.distance_to_goal == second_result.distance_to_goal

    # Test cache miss performance with unique coordinate pairs
    unique_pairs = [(Coordinates(i, i), Coordinates(i + 1, i + 1)) for i in range(10)]
    cache_miss_times = []

    for agent_pos, source_pos in unique_pairs:
        start_time = time.perf_counter()
        calculator.calculate_reward(agent_pos, source_pos)
        cache_miss_times.append(time.perf_counter() - start_time)

    # Validate cache size limits and LRU eviction behavior
    _ = len(calculator.distance_cache)

    # Fill cache beyond typical usage
    for i in range(CACHE_TEST_SIZE):
        agent_pos = Coordinates(i, i)
        source_pos = Coordinates(i + 100, i + 100)
        calculator.get_distance_to_goal(agent_pos, source_pos, use_cache=True)

    # Verify cache doesn't exceed maximum size
    assert len(calculator.distance_cache) <= 1000  # REWARD_CALCULATION_CACHE_SIZE

    # Test cache clearing and memory cleanup functionality
    clear_report = calculator.clear_cache()
    assert clear_report["cache_entries_cleared"] > 0
    assert clear_report["cache_size_after"] == 0
    assert len(calculator.distance_cache) == 0

    # Measure cache efficiency with realistic usage patterns
    repeated_positions = [Coordinates(1, 1), Coordinates(2, 2), Coordinates(1, 1)]
    source = Coordinates(10, 10)

    cache_times = []
    for pos in repeated_positions:
        start_time = time.perf_counter()
        calculator.get_distance_to_goal(pos, source, use_cache=True)
        cache_times.append(time.perf_counter() - start_time)

    # Third calculation (repeat of first) should be fastest due to cache hit
    if len(cache_times) >= 3:
        assert cache_times[2] <= cache_times[0], "Cache hit should improve performance"

    # Verify cache provides significant performance improvement
    no_cache_calculator = RewardCalculator(
        RewardCalculatorConfig(
            goal_radius=1.0,
            reward_goal_reached=1.0,
            reward_default=0.0,
            enable_caching=False,
        )
    )

    # Compare cached vs non-cached performance
    cached_time = time.perf_counter()
    calculator.get_distance_to_goal(test_agent, test_source, use_cache=True)
    _ = time.perf_counter() - cached_time

    no_cache_time = time.perf_counter()
    no_cache_calculator.get_distance_to_goal(test_agent, test_source, use_cache=False)
    _ = time.perf_counter() - no_cache_time

    # Cache should provide measurable benefit for repeated calculations
    cache_stats = calculator.get_reward_statistics(include_cache_statistics=True)
    assert "cache_statistics" in cache_stats


@pytest.mark.unit
def test_reward_statistics_tracking():
    """
    Test reward calculation statistics collection including operation counts, performance
    metrics, and goal achievement analysis with comprehensive tracking validation.
    """
    config = RewardCalculatorConfig(
        goal_radius=2.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        enable_performance_monitoring=True,
    )

    # Create reward calculator with statistics tracking enabled
    calculator = RewardCalculator(config)

    initial_stats = calculator.get_reward_statistics()
    assert initial_stats["total_calculations"] == 0
    assert initial_stats["goals_achieved"] == 0
    assert initial_stats["goal_achievement_rate"] == 0.0

    # Perform series of reward calculations with mixed goal achievement
    test_scenarios = [
        (Coordinates(0, 0), Coordinates(0, 0)),  # Goal achieved
        (Coordinates(0, 0), Coordinates(5, 5)),  # Goal not achieved
        (Coordinates(1, 1), Coordinates(1, 1)),  # Goal achieved
        (Coordinates(0, 0), Coordinates(10, 10)),  # Goal not achieved
        (Coordinates(2, 2), Coordinates(3, 3)),  # Goal achieved (distance ≈ 1.41 < 2.0)
    ]

    for agent_pos, source_pos in test_scenarios:
        calculator.calculate_reward(agent_pos, source_pos)

    # Retrieve reward statistics using get_reward_statistics
    final_stats = calculator.get_reward_statistics()

    # Verify calculation count tracking matches actual calculations performed
    assert final_stats["total_calculations"] == len(test_scenarios)
    assert calculator.calculation_count == len(test_scenarios)

    # Validate goal achievement rate calculation and accuracy
    expected_goals_achieved = 3  # First, third, and fifth scenarios
    assert final_stats["goals_achieved"] == expected_goals_achieved

    expected_rate = expected_goals_achieved / len(test_scenarios)
    assert abs(final_stats["goal_achievement_rate"] - expected_rate) < 1e-10

    # Test performance statistics include timing data and optimization metrics
    performance_stats = calculator.get_reward_statistics(
        include_performance_analysis=True
    )
    assert "performance_analysis" in performance_stats
    assert "performance_compliance_rate" in performance_stats["performance_analysis"]
    assert "target_latency_ms" in performance_stats["performance_analysis"]

    # Verify statistics include configuration information
    assert "configuration" in final_stats
    config_info = final_stats["configuration"]
    assert config_info["goal_radius"] == 2.0
    assert config_info["reward_goal_reached"] == 1.0
    assert config_info["reward_default"] == 0.0

    # Test average distance calculation in statistics
    assert "average_distance" in final_stats
    assert isinstance(final_stats["average_distance"], float)
    assert final_stats["average_distance"] >= 0.0

    # Verify statistics reset functionality clears tracking data properly
    # Note: RewardCalculator doesn't have explicit reset method, but clear_cache affects some stats
    _ = len(calculator.distance_cache)
    _ = calculator.clear_cache()

    post_clear_stats = calculator.get_reward_statistics(include_cache_statistics=True)
    if "cache_statistics" in post_clear_stats:
        assert post_clear_stats["cache_statistics"]["cache_size"] == 0


@pytest.mark.edge_case
@pytest.mark.skip(reason="Validation removed in core types simplification")
def test_parameter_validation_comprehensive():
    """
    Test comprehensive parameter validation for all reward calculator methods with edge
    cases and error conditions ensuring robust input validation and error reporting.
    """
    config = RewardCalculatorConfig(
        goal_radius=1.0, reward_goal_reached=1.0, reward_default=0.0
    )
    calculator = RewardCalculator(config)

    # Test calculate_reward with None coordinates raises ValidationError
    with pytest.raises(ValidationError):
        calculator.calculate_reward(None, Coordinates(0, 0))

    with pytest.raises(ValidationError):
        calculator.calculate_reward(Coordinates(0, 0), None)

    # Test calculate_reward with invalid coordinate types raises appropriate exception
    class InvalidCoordinate:
        def __init__(self):
            self.x = "invalid"
            self.y = "invalid"

    invalid_coord = InvalidCoordinate()
    with pytest.raises((ValidationError, AttributeError, TypeError)):
        calculator.calculate_reward(invalid_coord, Coordinates(0, 0))

    # Test check_termination with inconsistent agent state raises StateError
    class InconsistentAgentState:
        def __init__(self):
            self.step_count = 50
            # Missing position attribute

    inconsistent_agent = InconsistentAgentState()
    with pytest.raises(ValidationError):
        calculator.check_termination(inconsistent_agent, Coordinates(0, 0), 100)

    # Test negative step counts - should raise ValidationError during construction
    with pytest.raises(ValidationError):
        _ = AgentState(position=Coordinates(0, 0), step_count=-1, goal_reached=False)

    # Test extremely large coordinate values for numerical stability
    large_coord = Coordinates(999999999, 999999999)
    very_large_coord = Coordinates(1000000000, 1000000000)

    try:
        result = calculator.calculate_reward(large_coord, very_large_coord)
        assert isinstance(result.distance_to_goal, float)
        assert math.isfinite(result.distance_to_goal)
    except OverflowError:
        # Acceptable for extremely large values
        pass

    # Verify error messages provide specific parameter constraint information
    try:
        _ = RewardCalculatorConfig(
            goal_radius=-5.0, reward_goal_reached=1.0, reward_default=0.0
        )
    except ValidationError as e:
        assert "goal_radius" in str(e)
        assert "non-negative" in str(e)

    # Test parameter validation with boundary values and edge cases
    boundary_config = RewardCalculatorConfig(
        goal_radius=0.0,
        reward_goal_reached=1.0,
        reward_default=0.0,  # Exact boundary
    )

    boundary_calculator = RewardCalculator(boundary_config)

    # Test validation with exact matching coordinates
    exact_match = boundary_calculator.calculate_reward(
        Coordinates(5, 5), Coordinates(5, 5)
    )
    assert exact_match.goal_reached is True

    # Test validation with minimal difference
    minimal_diff = boundary_calculator.calculate_reward(
        Coordinates(5, 5), Coordinates(5, 6)
    )
    assert minimal_diff.goal_reached is False


@pytest.mark.unit
def test_configuration_cloning_and_modification():
    """
    Test RewardCalculatorConfig cloning and modification functionality ensuring immutability
    and parameter override handling with comprehensive validation of cloned configurations.
    """
    # Create original RewardCalculatorConfig with specific parameters
    original_config = RewardCalculatorConfig(
        goal_radius=5.0,
        reward_goal_reached=2.0,
        reward_default=-0.5,
        distance_calculation_method="euclidean",
        distance_precision=1e-8,
        enable_performance_monitoring=False,
        enable_caching=False,
        custom_parameters={"test_param": "test_value", "numeric_param": 42},
    )

    # Clone configuration without modifications using clone() method
    cloned_config = original_config.clone()

    # Verify cloned configuration has identical parameters to original
    assert cloned_config.goal_radius == original_config.goal_radius
    assert cloned_config.reward_goal_reached == original_config.reward_goal_reached
    assert cloned_config.reward_default == original_config.reward_default
    assert (
        cloned_config.distance_calculation_method
        == original_config.distance_calculation_method
    )
    assert cloned_config.distance_precision == original_config.distance_precision
    assert (
        cloned_config.enable_performance_monitoring
        == original_config.enable_performance_monitoring
    )
    assert cloned_config.enable_caching == original_config.enable_caching
    assert cloned_config.custom_parameters == original_config.custom_parameters

    # Verify the clone is a separate object (not the same reference)
    assert cloned_config is not original_config
    assert cloned_config.custom_parameters is not original_config.custom_parameters

    # Clone configuration with parameter overrides using overrides dictionary
    overrides = {
        "goal_radius": 10.0,
        "reward_goal_reached": 5.0,
        "enable_performance_monitoring": True,
        "new_custom_param": "override_value",
    }

    overridden_config = original_config.clone(overrides)

    # Validate override parameters are applied correctly in cloned configuration
    assert overridden_config.goal_radius == 10.0
    assert overridden_config.reward_goal_reached == 5.0
    assert overridden_config.enable_performance_monitoring is True
    assert overridden_config.custom_parameters["new_custom_param"] == "override_value"

    # Verify non-overridden parameters remain unchanged
    assert overridden_config.reward_default == original_config.reward_default
    assert (
        overridden_config.distance_calculation_method
        == original_config.distance_calculation_method
    )
    assert overridden_config.enable_caching == original_config.enable_caching

    # Test original configuration remains unchanged after cloning with overrides
    assert original_config.goal_radius == 5.0
    assert original_config.reward_goal_reached == 2.0
    assert original_config.enable_performance_monitoring is False
    assert "new_custom_param" not in original_config.custom_parameters

    # Verify cloned configuration passes validation with override parameters
    assert overridden_config.validate() is True

    # Test cloning with preserve_custom_parameters=False
    clean_clone = original_config.clone(preserve_custom_parameters=False)
    assert len(clean_clone.custom_parameters) == 0

    # Test cloning with invalid overrides raises validation error
    invalid_overrides = {"goal_radius": -1.0}
    with pytest.raises(ValidationError):
        original_config.clone(invalid_overrides)

    # Test deep copying behavior with nested custom parameters
    nested_config = RewardCalculatorConfig(
        goal_radius=1.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        custom_parameters={"nested": {"inner": "value"}},
    )

    nested_clone = nested_config.clone()
    nested_clone.custom_parameters["nested"]["inner"] = "modified"

    # Original should remain unchanged (deep copy)
    assert nested_config.custom_parameters["nested"]["inner"] == "value"


@pytest.mark.unit
def test_performance_impact_estimation():
    """
    Test performance impact estimation for different configuration parameters with
    resource usage analysis and optimization recommendation generation.
    """
    # Create configurations with different goal radius and precision settings
    configs = [
        RewardCalculatorConfig(
            goal_radius=0.0,
            reward_goal_reached=1.0,
            reward_default=0.0,
            enable_caching=True,
            distance_precision=1e-6,
        ),
        RewardCalculatorConfig(
            goal_radius=5.0,
            reward_goal_reached=1.0,
            reward_default=0.0,
            enable_caching=False,
            distance_precision=1e-12,
            distance_calculation_method="manhattan",
        ),
        RewardCalculatorConfig(
            goal_radius=2.0,
            reward_goal_reached=1.0,
            reward_default=0.0,
            enable_caching=True,
            distance_precision=1e-8,
            distance_calculation_method="chebyshev",
        ),
    ]

    expected_episode_length = 500

    for i, config in enumerate(configs):
        # Estimate performance impact using estimate_performance_impact method
        impact = config.estimate_performance_impact(
            expected_episode_length, include_caching_benefits=True
        )

        # Verify estimation provides timing projections and resource usage
        required_keys = [
            "per_step_calculation_ms",
            "total_episode_overhead_ms",
            "caching_benefit_factor",
            "performance_target_compliance",
            "optimization_recommendations",
        ]

        for key in required_keys:
            assert (
                key in impact
            ), f"Missing key '{key}' in performance impact for config {i}"

        # Validate timing estimates are positive and reasonable
        assert impact["per_step_calculation_ms"] > 0
        assert impact["total_episode_overhead_ms"] > 0
        assert (
            impact["per_step_calculation_ms"] * expected_episode_length
            <= impact["total_episode_overhead_ms"] * 1.2
        )  # Allow some overhead

        # Check caching benefit factor is reasonable
        if config.enable_caching:
            assert 0.1 <= impact["caching_benefit_factor"] <= 1.0
        else:
            assert impact["caching_benefit_factor"] == 1.0

        # Verify performance target compliance is boolean
        assert isinstance(impact["performance_target_compliance"], bool)

        # Validate optimization recommendations are provided
        assert isinstance(impact["optimization_recommendations"], list)
        assert len(impact["optimization_recommendations"]) > 0

        # Test estimation accuracy by comparing with actual performance measurements
        # (This would be a basic validation - full accuracy testing would require actual measurements)
        target_per_step = PERFORMANCE_TARGET_STEP_LATENCY_MS / 2

        if config.enable_caching and config.distance_calculation_method == "euclidean":
            # Optimal configuration should have good performance estimate
            assert impact["per_step_calculation_ms"] < target_per_step
            assert impact["performance_target_compliance"] is True

        # Validate caching benefits estimation when caching is enabled
        if config.enable_caching:
            caching_impact = config.estimate_performance_impact(
                expected_episode_length, include_caching_benefits=True
            )
            no_caching_impact = config.estimate_performance_impact(
                expected_episode_length, include_caching_benefits=False
            )

            # With caching benefits should be faster
            assert (
                caching_impact["per_step_calculation_ms"]
                <= no_caching_impact["per_step_calculation_ms"]
            )

        # Test optimization recommendations based on configuration parameters
        recommendations = impact["optimization_recommendations"]

        if not config.enable_caching:
            caching_rec = any("caching" in rec.lower() for rec in recommendations)
            assert caching_rec, "Should recommend enabling caching when disabled"

        if config.distance_calculation_method != "euclidean":
            euclidean_rec = any("euclidean" in rec.lower() for rec in recommendations)
            assert (
                euclidean_rec
            ), "Should recommend euclidean method for optimal performance"


@pytest.mark.unit
def test_reward_result_data_structure():
    """
    Test RewardResult data structure functionality including performance metrics and
    serialization with comprehensive data integrity validation.
    """
    # Create RewardResult with basic reward, goal status, and distance
    basic_result = RewardResult(reward=1.0, goal_reached=True, distance_to_goal=0.5)

    # Verify basic attributes are set correctly
    assert basic_result.reward == 1.0
    assert basic_result.goal_reached is True
    assert basic_result.distance_to_goal == 0.5
    assert basic_result.calculation_time_ms == 0.0
    assert isinstance(basic_result.calculation_details, dict)
    assert basic_result.goal_achievement_reason is None

    # Test set_performance_metrics adds timing data correctly
    timing_ms = 0.123
    details = {"method": "euclidean", "cached": False, "iterations": 1}

    basic_result.set_performance_metrics(timing_ms, details)

    assert basic_result.calculation_time_ms == timing_ms
    assert basic_result.calculation_details["method"] == "euclidean"
    assert basic_result.calculation_details["cached"] is False
    assert basic_result.calculation_details["iterations"] == 1

    # Verify to_dict method produces comprehensive dictionary representation
    result_dict = basic_result.to_dict(include_performance_data=False)

    expected_keys = ["reward", "goal_reached", "distance_to_goal"]
    for key in expected_keys:
        assert key in result_dict

    assert result_dict["reward"] == 1.0
    assert result_dict["goal_reached"] is True
    assert result_dict["distance_to_goal"] == 0.5

    # Test dictionary with performance data included
    perf_dict = basic_result.to_dict(include_performance_data=True)

    performance_keys = ["calculation_time_ms", "calculation_details"]
    for key in performance_keys:
        assert key in perf_dict

    assert perf_dict["calculation_time_ms"] == timing_ms
    assert perf_dict["calculation_details"] == basic_result.calculation_details

    # Test RewardResult immutability and data integrity
    _ = basic_result.reward
    _ = basic_result.goal_reached
    _ = basic_result.distance_to_goal

    # Attempting to modify should not affect original (dataclass is mutable, but test behavior)
    basic_result.reward = 2.0
    assert basic_result.reward == 2.0  # dataclass fields are mutable

    # Test with goal achievement reason
    result_with_reason = RewardResult(
        reward=0.0,
        goal_reached=False,
        distance_to_goal=5.0,
        goal_achievement_reason="Distance too large: 5.0 > 2.0",
    )

    reason_dict = result_with_reason.to_dict()
    assert "goal_achievement_reason" in reason_dict
    assert reason_dict["goal_achievement_reason"] == "Distance too large: 5.0 > 2.0"

    # Validate performance metrics integration and data consistency
    perf_result = RewardResult(reward=1.0, goal_reached=True, distance_to_goal=1.5)

    # Test performance warning when exceeding target
    slow_timing = PERFORMANCE_TARGET_STEP_LATENCY_MS * 2
    perf_result.set_performance_metrics(slow_timing, {"test": "data"})

    assert "performance_warning" in perf_result.calculation_details
    assert (
        perf_result.calculation_details["performance_warning"]
        == "Exceeded target latency"
    )

    # Test serialization compatibility for external analysis tools
    serialized = perf_result.to_dict(include_performance_data=True)

    # Ensure all values are JSON serializable
    import json

    try:
        json_str = json.dumps(serialized)
        deserialized = json.loads(json_str)
        assert deserialized["reward"] == perf_result.reward
        assert deserialized["goal_reached"] == perf_result.goal_reached
    except (TypeError, ValueError) as e:
        pytest.fail(f"RewardResult serialization failed: {e}")

    # Test validation of invalid inputs
    with pytest.raises(ValidationError):
        RewardResult(reward=float("nan"), goal_reached=True, distance_to_goal=1.0)

    with pytest.raises(ValidationError):
        RewardResult(reward=1.0, goal_reached="not_boolean", distance_to_goal=1.0)

    with pytest.raises(ValidationError):
        RewardResult(
            reward=1.0,
            goal_reached=True,
            distance_to_goal=-1.0,  # Negative distance
        )


@pytest.mark.unit
def test_termination_result_analysis():
    """
    Test TerminationResult comprehensive analysis including final state information and
    episode summary generation with comprehensive lifecycle management validation.
    """
    # Create TerminationResult for goal achievement scenario
    goal_termination = TerminationResult(
        terminated=True,
        truncated=False,
        termination_reason="Goal achieved: distance 0.5 <= radius 1.0",
    )

    # Verify basic initialization
    assert goal_termination.terminated is True
    assert goal_termination.truncated is False
    assert "Goal achieved" in goal_termination.termination_reason
    assert goal_termination.final_distance is None
    assert goal_termination.final_step_count is None
    assert isinstance(goal_termination.termination_details, dict)

    # Set final state information including distance and step count
    goal_termination.set_final_state(
        final_distance=0.5,
        final_step_count=42,
        additional_details={"goal_radius": 1.0, "reward": 1.0},
    )

    assert goal_termination.final_distance == 0.5
    assert goal_termination.final_step_count == 42
    assert goal_termination.termination_details["goal_radius"] == 1.0
    assert goal_termination.termination_details["reward"] == 1.0

    # Generate episode summary using get_summary method
    summary = goal_termination.get_summary()

    # Verify summary contains comprehensive episode completion details
    assert "TERMINATED" in summary
    assert "Goal achieved" in summary
    assert "0.500" in summary  # Final distance with 3 decimal places
    assert "42" in summary  # Step count

    # Test termination result for step limit truncation scenario
    truncation_termination = TerminationResult(
        terminated=False,
        truncated=True,
        termination_reason="Step limit reached: 1000 >= 1000",
    )

    truncation_termination.set_final_state(
        final_distance=5.7,
        final_step_count=1000,
        additional_details={"max_steps": 1000, "goal_radius": 2.0},
    )

    truncation_summary = truncation_termination.get_summary()
    assert "TRUNCATED" in truncation_summary
    assert "Step limit reached" in truncation_summary
    assert "5.700" in truncation_summary
    assert "1000" in truncation_summary

    # Validate different termination reasons produce appropriate summaries
    ongoing_termination = TerminationResult(
        terminated=False, truncated=False, termination_reason="Episode ongoing"
    )

    ongoing_termination.set_final_state(2.3, 150)
    ongoing_summary = ongoing_termination.get_summary()

    assert "ONGOING" in ongoing_summary or "Episode ongoing" in ongoing_summary
    assert "2.300" in ongoing_summary
    assert "150" in ongoing_summary

    # Test both terminated and truncated scenario (should prioritize terminated)
    both_termination = TerminationResult(
        terminated=True,
        truncated=True,
        termination_reason="Goal achieved at step limit",
    )

    both_summary = both_termination.get_summary()
    assert "TERMINATED" in both_summary
    assert "TRUNCATED" in both_summary

    # Test termination result data consistency and completeness
    assert isinstance(goal_termination.terminated, bool)
    assert isinstance(goal_termination.truncated, bool)
    assert isinstance(goal_termination.termination_reason, str)
    assert len(goal_termination.termination_reason) > 0

    # Validate numeric data types for final state
    assert isinstance(goal_termination.final_distance, float)
    assert isinstance(goal_termination.final_step_count, int)
    assert goal_termination.final_distance >= 0.0
    assert goal_termination.final_step_count >= 0

    # Test error handling for invalid termination results
    with pytest.raises(ValidationError):
        TerminationResult(
            terminated="not_boolean", truncated=False, termination_reason="Valid reason"
        )

    with pytest.raises(ValidationError):
        TerminationResult(
            terminated=True, truncated="not_boolean", termination_reason="Valid reason"
        )

    with pytest.raises(ValidationError):
        TerminationResult(
            terminated=True,
            truncated=False,
            termination_reason="",  # Empty reason
        )

    # Test set_final_state with invalid parameters
    test_termination = TerminationResult(
        terminated=True, truncated=False, termination_reason="Test"
    )

    # Negative values should be handled gracefully or rejected
    test_termination.set_final_state(-1.0, -5, {"test": "value"})
    # The implementation allows negative values, but they are converted appropriately
    assert test_termination.final_distance is not None
    assert test_termination.final_step_count is not None


@pytest.mark.unit
def test_factory_function_comprehensive():
    """
    Test create_reward_calculator factory function with various configuration options
    and validation scenarios ensuring proper component initialization and integration.
    """
    # Test factory function with default configuration creates valid calculator
    default_calculator = create_reward_calculator()

    assert isinstance(default_calculator, RewardCalculator)
    assert default_calculator.config.goal_radius == DEFAULT_GOAL_RADIUS
    assert default_calculator.config.reward_goal_reached == REWARD_GOAL_REACHED
    assert default_calculator.config.reward_default == REWARD_DEFAULT
    assert default_calculator.config.enable_performance_monitoring is True

    # Test factory function with custom configuration applies parameters correctly
    custom_config = RewardCalculatorConfig(
        goal_radius=3.0,
        reward_goal_reached=2.0,
        reward_default=-1.0,
        enable_caching=False,
    )

    custom_calculator = create_reward_calculator(
        config=custom_config,
        enable_performance_monitoring=False,
        enable_validation=True,
    )

    assert custom_calculator.config.goal_radius == 3.0
    assert custom_calculator.config.reward_goal_reached == 2.0
    assert custom_calculator.config.reward_default == -1.0
    assert custom_calculator.config.enable_caching is False
    assert custom_calculator.config.enable_performance_monitoring is False

    # Verify factory function enables performance monitoring when requested
    perf_calculator = create_reward_calculator(
        enable_performance_monitoring=True, enable_validation=False
    )

    assert perf_calculator.config.enable_performance_monitoring is True
    assert isinstance(perf_calculator.performance_metrics, dict)

    # Test factory function enables validation when requested
    # Validation happens during creation, so invalid config should raise error
    invalid_config = RewardCalculatorConfig.__new__(RewardCalculatorConfig)
    object.__setattr__(invalid_config, "goal_radius", -1.0)
    object.__setattr__(invalid_config, "reward_goal_reached", 1.0)
    object.__setattr__(invalid_config, "reward_default", 0.0)
    object.__setattr__(invalid_config, "distance_calculation_method", "euclidean")
    object.__setattr__(invalid_config, "distance_precision", 1e-12)
    object.__setattr__(invalid_config, "enable_performance_monitoring", True)
    object.__setattr__(invalid_config, "enable_caching", True)
    object.__setattr__(invalid_config, "custom_parameters", {})

    # Validate factory function handles invalid configuration by raising appropriate exceptions
    with pytest.raises(ValidationError):
        create_reward_calculator(config=invalid_config, enable_validation=True)

    # Should work without validation
    try:
        no_validation_calculator = create_reward_calculator(
            config=invalid_config, enable_validation=False
        )
        assert isinstance(no_validation_calculator, RewardCalculator)
    except ValidationError:
        # This might still fail if validation happens in RewardCalculatorConfig.__post_init__
        pass

    # Test factory function returns fully functional RewardCalculator instance
    functional_calculator = create_reward_calculator()

    # Test basic functionality
    test_agent = Coordinates(0, 0)
    test_source = Coordinates(0, 0)

    result = functional_calculator.calculate_reward(test_agent, test_source)
    assert isinstance(result, RewardResult)
    assert result.reward == REWARD_GOAL_REACHED  # At same position
    assert result.goal_reached is True

    # Test termination checking
    test_agent_state = AgentState(
        position=test_agent, step_count=10, goal_reached=False
    )

    termination = functional_calculator.check_termination(
        test_agent_state, test_source, 100
    )
    assert isinstance(termination, TerminationResult)

    # Test validation parameter affects internal validation
    validated_calculator = create_reward_calculator(enable_validation=True)

    # Should perform validation correctly
    assert (
        validated_calculator.validate_calculation_parameters(
            Coordinates(5, 5), Coordinates(10, 10)
        )
        is True
    )

    # Test with performance metrics disabled
    no_perf_calculator = create_reward_calculator(enable_performance_monitoring=False)

    assert isinstance(no_perf_calculator.performance_metrics, dict)
    assert no_perf_calculator.config.enable_performance_monitoring is False

    # Test factory creates independent instances
    calc1 = create_reward_calculator()
    calc2 = create_reward_calculator()

    assert calc1 is not calc2
    assert calc1.config is not calc2.config
    assert calc1.distance_cache is not calc2.distance_cache


@pytest.mark.unit
def test_configuration_validation_function():
    """
    Test validate_reward_config standalone function with comprehensive parameter checking
    and reporting ensuring robust configuration validation and error analysis.
    """
    # Test validate_reward_config with valid configuration returns positive validation result
    valid_config = RewardCalculatorConfig(
        goal_radius=2.0, reward_goal_reached=1.0, reward_default=0.0
    )

    is_valid, report = validate_reward_config(valid_config)

    assert is_valid is True
    assert isinstance(report, dict)
    assert report["is_valid"] is True
    assert len(report["errors"]) == 0

    # Test validation function with invalid parameters returns detailed error report
    invalid_config = RewardCalculatorConfig.__new__(RewardCalculatorConfig)
    object.__setattr__(invalid_config, "goal_radius", -2.0)
    object.__setattr__(invalid_config, "reward_goal_reached", float("inf"))
    object.__setattr__(invalid_config, "reward_default", 0.0)
    object.__setattr__(invalid_config, "distance_calculation_method", "euclidean")
    object.__setattr__(invalid_config, "distance_precision", 1e-12)
    object.__setattr__(invalid_config, "enable_performance_monitoring", True)
    object.__setattr__(invalid_config, "enable_caching", True)
    object.__setattr__(invalid_config, "custom_parameters", {})

    is_valid_invalid, report_invalid = validate_reward_config(invalid_config)

    assert is_valid_invalid is False
    assert isinstance(report_invalid, dict)
    assert report_invalid["is_valid"] is False
    assert len(report_invalid["errors"]) > 0

    # Check specific error messages
    error_messages = " ".join(report_invalid["errors"])
    assert "goal_radius" in error_messages

    # Verify strict validation mode applies additional constraint checking
    borderline_config = RewardCalculatorConfig(
        goal_radius=0.1,
        reward_goal_reached=1.0,
        reward_default=0.9999999,  # Very close to goal reward
    )

    normal_valid, normal_report = validate_reward_config(
        borderline_config, strict_validation=False
    )
    strict_valid, strict_report = validate_reward_config(
        borderline_config, strict_validation=True
    )

    # Strict validation should be more stringent
    if normal_valid and not strict_valid:
        assert len(strict_report["errors"]) > len(normal_report["errors"])

    # Test validation context parameter influences validation behavior
    context_config = RewardCalculatorConfig(
        goal_radius=50.0,
        reward_goal_reached=1.0,
        reward_default=0.0,  # Large but valid
    )

    no_context_valid, no_context_report = validate_reward_config(context_config)

    validation_context = {"max_goal_radius": 10.0, "environment": "test"}

    context_valid, context_report = validate_reward_config(
        context_config, validation_context=validation_context
    )

    # Context might influence validation (implementation dependent)
    assert isinstance(context_report, dict)

    # Validate error reporting includes specific parameter constraints and suggestions
    detailed_invalid_config = RewardCalculatorConfig(
        goal_radius=1001.0,  # Exceeds maximum
        reward_goal_reached=1.0,
        reward_default=1.0,  # Same as goal reward
    )

    detailed_invalid_config._RewardCalculatorConfig__post_init = lambda: None

    _, detailed_report = validate_reward_config(
        detailed_invalid_config, strict_validation=True
    )

    # Should include parameter constraints in report
    assert "parameter_analysis" in detailed_report
    assert isinstance(detailed_report["parameter_analysis"], dict)

    # Should include recommendations
    assert "recommendations" in detailed_report
    assert isinstance(detailed_report["recommendations"], list)

    # Test validation function integration with RewardCalculatorConfig.validate method
    integrated_config = RewardCalculatorConfig(
        goal_radius=1.0, reward_goal_reached=1.0, reward_default=0.0
    )

    # Both methods should give consistent results for valid configs
    standalone_valid, standalone_report = validate_reward_config(integrated_config)

    try:
        method_valid = integrated_config.validate()
        assert standalone_valid == method_valid
    except ValidationError:
        assert standalone_valid is False

    # Test performance analysis inclusion
    perf_config = RewardCalculatorConfig(
        goal_radius=1.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        enable_performance_monitoring=True,
        enable_caching=True,
    )

    _, perf_report = validate_reward_config(perf_config)

    if "parameter_analysis" in perf_report:
        param_analysis = perf_report["parameter_analysis"]
        assert "performance_estimate" in param_analysis
        assert isinstance(param_analysis["performance_estimate"], dict)

    # Test edge cases and error handling in validation function
    try:
        none_config = None
        is_valid_none, report_none = validate_reward_config(none_config)
        assert is_valid_none is False
        assert len(report_none["errors"]) > 0
    except (TypeError, AttributeError):
        # Expected for None input
        pass


@pytest.mark.reproducibility
def test_reproducibility_with_seeding():
    """
    Test reward calculator reproducibility with deterministic seeding ensuring identical
    calculations across runs with comprehensive deterministic behavior validation.
    """
    # Create two identical reward calculators with same configuration
    config = RewardCalculatorConfig(
        goal_radius=2.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        enable_caching=True,
        enable_performance_monitoring=True,
    )

    calculator1 = RewardCalculator(config)
    calculator2 = RewardCalculator(config)

    # Define identical sequence of reward calculations
    test_sequence = [
        (Coordinates(0, 0), Coordinates(0, 0)),
        (Coordinates(1, 1), Coordinates(3, 4)),
        (Coordinates(10, 5), Coordinates(10, 7)),
        (Coordinates(100, 100), Coordinates(102, 102)),
        (Coordinates(0, 10), Coordinates(10, 0)),
    ]

    results1 = []
    results2 = []

    # Perform identical sequence of reward calculations on both instances
    for agent_pos, source_pos in test_sequence:
        result1 = calculator1.calculate_reward(agent_pos, source_pos)
        result2 = calculator2.calculate_reward(agent_pos, source_pos)

        results1.append(result1)
        results2.append(result2)

    # Verify all reward calculations produce identical results
    for i, (r1, r2) in enumerate(zip(results1, results2)):
        assert (
            r1.reward == r2.reward
        ), f"Reward mismatch at index {i}: {r1.reward} != {r2.reward}"
        assert r1.goal_reached == r2.goal_reached, f"Goal status mismatch at index {i}"
        assert (
            abs(r1.distance_to_goal - r2.distance_to_goal) < DISTANCE_TOLERANCE
        ), f"Distance mismatch at index {i}"

    # Test distance calculations are deterministic across multiple runs
    test_agent = Coordinates(42, 24)
    test_source = Coordinates(50, 30)

    # Multiple calculations should yield identical results
    distances = []
    for _ in range(10):
        distance = calculator1.get_distance_to_goal(
            test_agent, test_source, use_cache=False
        )
        distances.append(distance)

    # All distances should be identical
    for distance in distances[1:]:
        assert abs(distance - distances[0]) < DISTANCE_TOLERANCE

    # Validate performance metrics show consistent timing patterns
    # Note: Timing will vary, but the algorithmic behavior should be consistent
    timing_variations = []

    for _ in range(5):
        start_time = time.perf_counter()
        calculator1.calculate_reward(test_agent, test_source)
        timing_variations.append(time.perf_counter() - start_time)

    # While timing varies, the calculations themselves should be deterministic
    # Test this by verifying repeated calculations with same inputs
    baseline_result = calculator1.calculate_reward(test_agent, test_source)

    for _ in range(5):
        repeat_result = calculator1.calculate_reward(test_agent, test_source)
        assert baseline_result.reward == repeat_result.reward
        assert baseline_result.goal_reached == repeat_result.goal_reached
        assert (
            abs(baseline_result.distance_to_goal - repeat_result.distance_to_goal)
            < DISTANCE_TOLERANCE
        )

    # Test cache behavior is deterministic with identical input sequences
    calculator3 = RewardCalculator(config)
    calculator4 = RewardCalculator(config)

    cache_test_sequence = [
        (Coordinates(1, 1), Coordinates(1, 1)),  # Will be cached
        (Coordinates(2, 2), Coordinates(2, 2)),  # Will be cached
        (Coordinates(1, 1), Coordinates(1, 1)),  # Cache hit
        (Coordinates(3, 3), Coordinates(3, 3)),  # New calculation
        (Coordinates(2, 2), Coordinates(2, 2)),  # Cache hit
    ]

    cache_results3 = []
    cache_results4 = []

    for agent_pos, source_pos in cache_test_sequence:
        result3 = calculator3.calculate_reward(agent_pos, source_pos)
        result4 = calculator4.calculate_reward(agent_pos, source_pos)

        cache_results3.append(result3)
        cache_results4.append(result4)

    # Cache behavior should be deterministic
    for i, (r3, r4) in enumerate(zip(cache_results3, cache_results4)):
        assert r3.reward == r4.reward, f"Cache test reward mismatch at index {i}"
        assert (
            r3.goal_reached == r4.goal_reached
        ), f"Cache test goal status mismatch at index {i}"
        assert (
            abs(r3.distance_to_goal - r4.distance_to_goal) < DISTANCE_TOLERANCE
        ), f"Cache test distance mismatch at index {i}"

    # Verify cache state is consistent between calculators
    assert len(calculator3.distance_cache) == len(calculator4.distance_cache)

    # Test deterministic behavior with statistics tracking
    stats3 = calculator3.get_reward_statistics()
    stats4 = calculator4.get_reward_statistics()

    assert stats3["total_calculations"] == stats4["total_calculations"]
    assert stats3["goals_achieved"] == stats4["goals_achieved"]
    assert (
        abs(stats3["goal_achievement_rate"] - stats4["goal_achievement_rate"])
        < DISTANCE_TOLERANCE
    )


@pytest.mark.performance
@pytest.mark.slow
def test_stress_testing_large_scale():
    """
    Test reward calculator under stress with large-scale calculations and memory pressure
    validation ensuring system stability and performance under load conditions.
    """
    config = RewardCalculatorConfig(
        goal_radius=5.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        enable_caching=True,
        enable_performance_monitoring=True,
    )

    # Create reward calculator with maximum cache size
    calculator = RewardCalculator(config)

    # Record initial memory state (simplified - would use memory profiling in practice)
    _ = len(calculator.distance_cache)

    # Generate random coordinates for stress testing using isolated RNG
    rng = np.random.default_rng(42)

    stress_coordinates = []
    for i in range(STRESS_TEST_ITERATIONS):
        agent_x = int(rng.integers(0, 1000))
        agent_y = int(rng.integers(0, 1000))
        source_x = int(rng.integers(0, 1000))
        source_y = int(rng.integers(0, 1000))

        stress_coordinates.append(
            (Coordinates(agent_x, agent_y), Coordinates(source_x, source_y))
        )

    # Perform STRESS_TEST_ITERATIONS calculations with random coordinates
    start_time = time.perf_counter()
    successful_calculations = 0
    timing_samples = []

    for i, (agent_pos, source_pos) in enumerate(stress_coordinates):
        try:
            calc_start = time.perf_counter()
            result = calculator.calculate_reward(agent_pos, source_pos)
            calc_end = time.perf_counter()

            # Validate each result
            assert isinstance(result, RewardResult)
            assert isinstance(result.reward, (int, float))
            assert isinstance(result.goal_reached, bool)
            assert result.distance_to_goal >= 0

            successful_calculations += 1

            # Sample timing periodically to avoid memory issues
            if i % 100 == 0:
                timing_samples.append((calc_end - calc_start) * 1000)

        except Exception as e:
            # Log but don't fail immediately - assess overall stability
            print(f"Calculation {i} failed: {e}")

    total_time = time.perf_counter() - start_time

    # Monitor memory usage during stress testing
    final_cache_size = len(calculator.distance_cache)

    # Verify performance remains within acceptable bounds under load
    average_time_per_calc = (total_time / successful_calculations) * 1000
    assert (
        average_time_per_calc < PERFORMANCE_TARGET_STEP_LATENCY_MS * 2
    ), f"Stress test average time {average_time_per_calc:.3f}ms exceeded threshold"

    # Sample timing analysis
    if timing_samples:
        sample_average = np.mean(timing_samples)
        assert (
            sample_average < PERFORMANCE_TARGET_STEP_LATENCY_MS * 2.0
        ), f"Sample timing {sample_average:.3f}ms too slow"

    # Test cache behavior under memory pressure and eviction scenarios
    # Cache should not grow indefinitely
    max_cache_size = 1000  # REWARD_CALCULATION_CACHE_SIZE from reward_calculator.py
    assert (
        final_cache_size <= max_cache_size
    ), f"Cache size {final_cache_size} exceeded maximum {max_cache_size}"

    # Validate system stability and error handling during stress conditions
    success_rate = successful_calculations / STRESS_TEST_ITERATIONS
    assert (
        success_rate > 0.95
    ), f"Success rate {success_rate:.3f} too low - system unstable under stress"

    # Test calculator state integrity after stress testing
    stats = calculator.get_reward_statistics()
    assert stats["total_calculations"] >= successful_calculations
    assert (
        stats["performance_violations"] / stats["total_calculations"] <= 1.0
    ), "Too many performance violations"

    # Test memory cleanup and stability
    pre_clear_cache_size = len(calculator.distance_cache)
    clear_report = calculator.clear_cache()

    assert clear_report["cache_entries_cleared"] == pre_clear_cache_size
    assert len(calculator.distance_cache) == 0

    # Generate stress test report with performance degradation analysis
    _ = {
        "total_iterations": STRESS_TEST_ITERATIONS,
        "successful_calculations": successful_calculations,
        "success_rate": success_rate,
        "total_time_seconds": total_time,
        "average_time_per_calc_ms": average_time_per_calc,
        "final_cache_size": final_cache_size,
        "cache_utilization": final_cache_size / max_cache_size,
        "performance_violations": stats["performance_violations"],
        "timing_samples": len(timing_samples),
    }

    # Validate no significant performance degradation
    if timing_samples and len(timing_samples) >= 10:
        early_samples = timing_samples[:5]
        late_samples = timing_samples[-5:]

        early_avg = np.mean(early_samples)
        late_avg = np.mean(late_samples)

        degradation_ratio = late_avg / early_avg if early_avg > 0 else 1.0
        assert (
            degradation_ratio < 2.0
        ), f"Performance degraded by factor {degradation_ratio:.2f}"

    # Test post-stress functionality
    post_stress_result = calculator.calculate_reward(
        Coordinates(0, 0), Coordinates(0, 0)
    )

    assert post_stress_result.reward == 1.0
    assert post_stress_result.goal_reached is True


@pytest.mark.edge_case
def test_mathematical_edge_cases():
    """
    Test mathematical edge cases including floating-point precision limits, extreme
    coordinates, and numerical stability ensuring robust mathematical operations.
    """
    config = RewardCalculatorConfig(
        goal_radius=1.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        distance_precision=1e-15,
    )
    calculator = RewardCalculator(config)

    # Test calculations with coordinates at floating-point precision limits
    epsilon = np.finfo(float).eps
    tiny_coord1 = Coordinates(int(epsilon * 1e10), int(epsilon * 1e10))
    tiny_coord2 = Coordinates(0, 0)

    try:
        tiny_result = calculator.calculate_reward(tiny_coord1, tiny_coord2)
        assert isinstance(tiny_result.distance_to_goal, float)
        assert math.isfinite(tiny_result.distance_to_goal)
        assert tiny_result.distance_to_goal >= 0
    except (OverflowError, FloatingPointError):
        # Acceptable for extreme precision limits
        pass

    # Test distance calculations with extremely large coordinate values
    large_max = 2**30  # Very large but not overflow-prone
    large_coord1 = Coordinates(large_max, large_max)
    large_coord2 = Coordinates(large_max - 1, large_max - 1)

    try:
        large_result = calculator.calculate_reward(large_coord1, large_coord2)
        assert isinstance(large_result.distance_to_goal, float)
        assert math.isfinite(large_result.distance_to_goal)

        # Should be approximately sqrt(2) ≈ 1.414
        expected_distance = math.sqrt(2)
        assert (
            abs(large_result.distance_to_goal - expected_distance)
            < DISTANCE_TOLERANCE * 10
        )

    except OverflowError:
        # Acceptable for extremely large values
        pass

    # Verify numerical stability with coordinates near zero
    near_zero_coords = [
        (Coordinates(0, 0), Coordinates(1, 0)),
        (Coordinates(1, 0), Coordinates(0, 0)),
        (Coordinates(0, 1), Coordinates(0, 0)),
        (Coordinates(0, 0), Coordinates(0, 1)),
    ]

    for coord1, coord2 in near_zero_coords:
        result = calculator.calculate_reward(coord1, coord2)
        assert math.isfinite(result.distance_to_goal)
        assert abs(result.distance_to_goal - 1.0) < DISTANCE_TOLERANCE

    # Test goal detection with goal_radius values near floating-point epsilon
    epsilon_config = RewardCalculatorConfig(
        goal_radius=epsilon * 1000,  # Very small but not zero
        reward_goal_reached=1.0,
        reward_default=0.0,
    )
    epsilon_calculator = RewardCalculator(epsilon_config)

    # Same coordinates should always achieve goal regardless of tiny goal_radius
    same_coords_result = epsilon_calculator.calculate_reward(
        Coordinates(5, 5), Coordinates(5, 5)
    )
    assert same_coords_result.goal_reached is True

    # Coordinates with tiny difference might or might not achieve goal depending on precision
    tiny_diff_result = epsilon_calculator.calculate_reward(
        Coordinates(5, 5),
        Coordinates(5, 5),  # Identical - should achieve
    )
    assert tiny_diff_result.goal_reached is True

    # Validate calculations handle NaN and infinity values appropriately
    # Note: These should be caught by validation, but test robustness
    try:
        nan_coord = Coordinates(0, 0)
        inf_coord = Coordinates(0, 0)

        # The coordinate creation itself should handle validation
        # If it doesn't, the calculator should handle it gracefully
        normal_result = calculator.calculate_reward(nan_coord, inf_coord)
        assert math.isfinite(normal_result.distance_to_goal)

    except (ValidationError, ValueError, OverflowError):
        # Expected behavior for invalid inputs
        pass

    # Test mathematical consistency across different coordinate ranges
    test_ranges = [
        (0, 10),  # Small coordinates
        (100, 110),  # Medium coordinates
        (10000, 10010),  # Large coordinates
    ]

    for x_min, x_max in test_ranges:
        for y_min, y_max in test_ranges:
            coord1 = Coordinates(x_min, y_min)
            coord2 = Coordinates(x_max, y_max)

            result = calculator.calculate_reward(coord1, coord2)

            # Verify mathematical consistency
            expected_distance = math.sqrt((x_max - x_min) ** 2 + (y_max - y_min) ** 2)
            assert abs(
                result.distance_to_goal - expected_distance
            ) < DISTANCE_TOLERANCE * max(1, expected_distance * 1e-10)

            # Test symmetry
            symmetric_result = calculator.calculate_reward(coord2, coord1)
            assert (
                abs(result.distance_to_goal - symmetric_result.distance_to_goal)
                < DISTANCE_TOLERANCE
            )

    # Verify precision handling maintains accuracy within DISTANCE_TOLERANCE
    precision_test_cases = [
        (Coordinates(0, 0), Coordinates(3, 4)),  # 3-4-5 triangle
        (Coordinates(0, 0), Coordinates(5, 12)),  # 5-12-13 triangle
        (Coordinates(0, 0), Coordinates(8, 15)),  # 8-15-17 triangle
    ]

    for coord1, coord2 in precision_test_cases:
        result = calculator.calculate_reward(coord1, coord2)

        # Calculate expected distance manually
        dx = coord2.x - coord1.x
        dy = coord2.y - coord1.y
        expected = math.sqrt(dx * dx + dy * dy)

        assert abs(result.distance_to_goal - expected) < DISTANCE_TOLERANCE

    # Test high precision mode with extreme precision requirements
    high_precision_result = calculator.get_distance_to_goal(
        Coordinates(1000000000, 1000000000),
        Coordinates(1000000001, 1000000001),
        use_cache=False,
        high_precision=True,
    )

    # Should be sqrt(2) regardless of large coordinate values
    assert (
        abs(high_precision_result - math.sqrt(2)) < DISTANCE_TOLERANCE * 100
    )  # Allow for some numerical error with large numbers


@pytest.mark.integration
def test_integration_with_performance_metrics():
    """Test reward calculator timing aggregation without external metrics classes."""
    config = RewardCalculatorConfig(
        goal_radius=2.0,
        reward_goal_reached=1.0,
        reward_default=0.0,
        enable_performance_monitoring=True,
    )
    calculator = RewardCalculator(config)

    assert isinstance(calculator.performance_metrics, dict)
    assert calculator.config.enable_performance_monitoring is True

    test_calculations = [
        (Coordinates(0, 0), Coordinates(0, 0)),
        (Coordinates(0, 0), Coordinates(5, 5)),
        (Coordinates(10, 10), Coordinates(12, 12)),
    ]

    for agent_pos, source_pos in test_calculations:
        result = calculator.calculate_reward(agent_pos, source_pos)
        assert isinstance(result.calculation_time_ms, (int, float))
        assert result.calculation_time_ms >= 0
        if result.calculation_details:
            assert "distance_calculation_method" in result.calculation_details
            assert "cache_enabled" in result.calculation_details

    timings = calculator.performance_metrics.get("reward_calculation", [])
    assert len(timings) >= len(test_calculations)

    calculator_stats = calculator.get_reward_statistics(
        include_performance_analysis=True
    )
    performance_analysis = calculator_stats["performance_analysis"]
    assert performance_analysis["monitoring_enabled"] is True

    method_configs = [
        (
            "euclidean",
            RewardCalculatorConfig(
                goal_radius=2.0,
                reward_goal_reached=1.0,
                reward_default=0.0,
                distance_calculation_method="euclidean",
            ),
        ),
        (
            "manhattan",
            RewardCalculatorConfig(
                goal_radius=2.0,
                reward_goal_reached=1.0,
                reward_default=0.0,
                distance_calculation_method="manhattan",
            ),
        ),
        (
            "chebyshev",
            RewardCalculatorConfig(
                goal_radius=2.0,
                reward_goal_reached=1.0,
                reward_default=0.0,
                distance_calculation_method="chebyshev",
            ),
        ),
    ]

    for method_name, method_config in method_configs:
        method_calculator = RewardCalculator(method_config)
        result = method_calculator.calculate_reward(
            Coordinates(10, 10), Coordinates(13, 14)
        )
        if result.calculation_details:
            assert (
                result.calculation_details.get("distance_calculation_method")
                == method_name
            )
        method_stats = method_calculator.get_reward_statistics(
            include_performance_analysis=True
        )
        assert method_stats["configuration"]["distance_method"] == method_name
