"""Tests for CloudWatch Application Signals MCP Server."""

import pytest
from awslabs.cloudwatch_applicationsignals_mcp_server.server import _filter_operation_targets, main
from awslabs.cloudwatch_applicationsignals_mcp_server.service_tools import (
    list_monitored_services,
    query_service_metrics,
)
from awslabs.cloudwatch_applicationsignals_mcp_server.slo_tools import get_slo
from awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools import (
    _compose_spans_query,
    check_transaction_search_enabled,
    list_slis,
    search_transaction_spans,
)
from awslabs.cloudwatch_applicationsignals_mcp_server.utils import remove_null_values
from botocore.exceptions import ClientError
from datetime import datetime, timedelta, timezone
from unittest.mock import AsyncMock, MagicMock, patch


@pytest.fixture(autouse=True)
def mock_aws_clients():
    """Mock all AWS clients to prevent real API calls during tests."""
    # Create mock clients
    mock_logs_client = MagicMock()
    mock_applicationsignals_client = MagicMock()
    mock_cloudwatch_client = MagicMock()
    mock_xray_client = MagicMock()
    mock_synthetics_client = MagicMock()
    mock_s3_client = MagicMock()

    # Patch the clients in all modules where they're imported
    patches = [
        # Original aws_clients module
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.aws_clients.logs_client',
            mock_logs_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.aws_clients.applicationsignals_client',
            mock_applicationsignals_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.aws_clients.cloudwatch_client',
            mock_cloudwatch_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.aws_clients.xray_client',
            mock_xray_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.aws_clients.synthetics_client',
            mock_synthetics_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.aws_clients.s3_client',
            mock_s3_client,
        ),
        # Service tools module (check what's actually imported)
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.service_tools.applicationsignals_client',
            mock_applicationsignals_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.service_tools.cloudwatch_client',
            mock_cloudwatch_client,
        ),
        # SLO tools module (check what's actually imported)
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.slo_tools.applicationsignals_client',
            mock_applicationsignals_client,
        ),
        # Trace tools module (logs_client, xray_client, and applicationsignals_client are imported)
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.logs_client',
            mock_logs_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.xray_client',
            mock_xray_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.applicationsignals_client',
            mock_applicationsignals_client,
        ),
        # SLI report client module (applicationsignals_client and cloudwatch_client are imported)
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.sli_report_client.applicationsignals_client',
            mock_applicationsignals_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.sli_report_client.cloudwatch_client',
            mock_cloudwatch_client,
        ),
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.synthetics_client',
            mock_synthetics_client,
        ),
        patch('awslabs.cloudwatch_applicationsignals_mcp_server.server.s3_client', mock_s3_client),
        patch('awslabs.cloudwatch_applicationsignals_mcp_server.server.iam_client', MagicMock()),
    ]

    # Start all patches
    for p in patches:
        p.start()

    try:
        yield {
            'logs_client': mock_logs_client,
            'applicationsignals_client': mock_applicationsignals_client,
            'cloudwatch_client': mock_cloudwatch_client,
            'xray_client': mock_xray_client,
            'synthetics_client': mock_synthetics_client,
            's3_client': mock_s3_client,
        }
    finally:
        # Stop all patches
        for p in patches:
            p.stop()


@pytest.fixture
def mock_mcp():
    """Mock the FastMCP instance."""
    with patch('awslabs.cloudwatch_applicationsignals_mcp_server.server.mcp') as mock:
        yield mock


@pytest.mark.asyncio
async def test_list_monitored_services_success(mock_aws_clients):
    """Test successful listing of monitored services."""
    mock_response = {
        'ServiceSummaries': [
            {
                'KeyAttributes': {
                    'Name': 'test-service',
                    'Type': 'AWS::ECS::Service',
                    'Environment': 'production',
                }
            }
        ]
    }

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_response

    result = await list_monitored_services()

    assert 'Application Signals Services (1 total' in result
    assert 'test-service' in result
    assert 'AWS::ECS::Service' in result


@pytest.mark.asyncio
async def test_list_monitored_services_empty(mock_aws_clients):
    """Test when no services are found."""
    mock_response = {'ServiceSummaries': []}

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_response

    result = await list_monitored_services()

    assert result == 'No services found in Application Signals.'


@pytest.mark.asyncio
async def test_query_service_metrics_success(mock_aws_clients):
    """Test successful query of service metrics."""
    mock_list_response = {
        'ServiceSummaries': [
            {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
        ]
    }

    mock_get_response = {
        'Service': {
            'MetricReferences': [
                {
                    'Namespace': 'AWS/ApplicationSignals',
                    'MetricName': 'Latency',
                    'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
                }
            ]
        }
    }

    mock_metric_response = {
        'Datapoints': [
            {
                'Timestamp': datetime.now(timezone.utc),
                'Average': 100.5,
                'p99': 150.2,
                'Unit': 'Milliseconds',
            }
        ]
    }

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_list_response
    mock_aws_clients['applicationsignals_client'].get_service.return_value = mock_get_response
    mock_aws_clients['cloudwatch_client'].get_metric_statistics.return_value = mock_metric_response

    result = await query_service_metrics(
        service_name='test-service',
        metric_name='Latency',
        statistic='Average',
        extended_statistic='p99',
        hours=1,
    )

    assert 'Metrics for test-service - Latency' in result
    assert 'Average Statistics:' in result
    assert 'p99 Statistics:' in result


@pytest.mark.asyncio
async def test_query_service_metrics_list_available(mock_aws_clients):
    """Test listing available metrics when no specific metric is requested."""
    mock_list_response = {
        'ServiceSummaries': [
            {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
        ]
    }

    mock_get_response = {
        'Service': {
            'MetricReferences': [
                {
                    'MetricName': 'Latency',
                    'Namespace': 'AWS/ApplicationSignals',
                    'MetricType': 'GAUGE',
                },
                {
                    'MetricName': 'Error',
                    'Namespace': 'AWS/ApplicationSignals',
                    'MetricType': 'COUNT',
                },
            ]
        }
    }

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_list_response
    mock_aws_clients['applicationsignals_client'].get_service.return_value = mock_get_response

    result = await query_service_metrics(
        service_name='test-service',
        metric_name='',  # Empty to list available metrics
        statistic='Average',
        extended_statistic='p99',
        hours=1,
    )

    assert "Available metrics for service 'test-service'" in result
    assert 'Latency' in result
    assert 'Error' in result


@pytest.mark.asyncio
async def test_get_slo_success(mock_aws_clients):
    """Test successful retrieval of SLO details."""
    mock_slo_response = {
        'Slo': {
            'Name': 'test-slo',
            'Arn': 'arn:aws:application-signals:us-east-1:123456789012:slo/test-slo',
            'Description': 'Test SLO for latency',
            'EvaluationType': 'REQUEST_BASED',
            'CreatedTime': '2024-01-01T00:00:00Z',
            'LastUpdatedTime': '2024-01-02T00:00:00Z',
            'Goal': {
                'AttainmentGoal': 99.9,
                'WarningThreshold': 99.0,
                'Interval': {'RollingInterval': {'Duration': 7, 'DurationUnit': 'DAYS'}},
            },
            'RequestBasedSli': {
                'RequestBasedSliMetric': {
                    'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'},
                    'OperationName': 'GET /api/test',
                    'MetricType': 'LATENCY',
                    'MetricDataQueries': [
                        {
                            'Id': 'query1',
                            'MetricStat': {
                                'Metric': {
                                    'Namespace': 'AWS/ApplicationSignals',
                                    'MetricName': 'Latency',
                                    'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
                                },
                                'Period': 60,
                                'Stat': 'Average',
                            },
                        }
                    ],
                },
                'MetricThreshold': 1000,
                'ComparisonOperator': 'GreaterThan',
            },
        }
    }

    mock_aws_clients[
        'applicationsignals_client'
    ].get_service_level_objective.return_value = mock_slo_response

    result = await get_slo('test-slo-id')

    assert 'Service Level Objective Details' in result
    assert 'test-slo' in result
    assert 'REQUEST_BASED' in result
    assert '99.9%' in result
    assert 'GET /api/test' in result
    assert 'annotation[aws.local.operation]="GET /api/test"' in result


@pytest.mark.asyncio
async def test_get_slo_not_found(mock_aws_clients):
    """Test when SLO is not found."""
    mock_aws_clients['applicationsignals_client'].get_service_level_objective.return_value = {
        'Slo': None
    }

    result = await get_slo('nonexistent-slo')

    assert 'No SLO found with ID: nonexistent-slo' in result


@pytest.mark.asyncio
async def test_search_transaction_spans_success(mock_aws_clients):
    """Test successful transaction search."""
    mock_query_response = {
        'queryId': 'test-query-id',
        'status': 'Complete',
        'statistics': {'recordsMatched': 10},
        'results': [
            [
                {'field': 'spanId', 'value': 'span1'},
                {'field': 'timestamp', 'value': '2024-01-01T00:00:00Z'},
            ]
        ],
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = mock_query_response

        result = await search_transaction_spans(
            log_group_name='aws/spans',
            start_time='2024-01-01T00:00:00+00:00',  # Fixed ISO format
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp, spanId',
            limit=100,
            max_timeout=30,
        )

        assert result['status'] == 'Complete'
        assert result['queryId'] == 'test-query-id'
        assert len(result['results']) == 1
        assert result['results'][0]['spanId'] == 'span1'


@pytest.mark.asyncio
async def test_search_transaction_spans_not_enabled(mock_aws_clients):
    """Test when transaction search is not enabled."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (False, 'XRay', 'INACTIVE')

        result = await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp',
            limit=None,
            max_timeout=30,
        )

        assert result['status'] == 'Transaction Search Not Available'
        assert not result['transaction_search_status']['enabled']


@pytest.mark.asyncio
async def test_list_slis_success(mock_aws_clients):
    """Test successful listing of SLI status."""
    mock_services_response = {
        'ServiceSummaries': [
            {
                'KeyAttributes': {
                    'Name': 'test-service',
                    'Type': 'AWS::ECS::Service',
                    'Environment': 'production',
                }
            }
        ]
    }

    # Mock boto3.client calls in SLIReportClient
    with patch('boto3.client') as mock_boto3_client:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
        ) as mock_check:
            # Configure boto3.client to return our mocked clients
            def boto3_client_side_effect(service_name, **kwargs):
                if service_name == 'application-signals':
                    return mock_aws_clients['applicationsignals_client']
                elif service_name == 'cloudwatch':
                    return mock_aws_clients['cloudwatch_client']
                else:
                    return MagicMock()

            mock_boto3_client.side_effect = boto3_client_side_effect

            mock_aws_clients[
                'applicationsignals_client'
            ].list_services.return_value = mock_services_response
            mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')

            # Mock SLO summaries response for SLIReportClient
            mock_slo_response = {
                'SloSummaries': [
                    {
                        'Name': 'test-slo',
                        'Arn': 'arn:aws:application-signals:us-east-1:123456789012:slo/test-slo',
                        'KeyAttributes': {'Name': 'test-service'},
                        'OperationName': 'GET /api',
                        'CreatedTime': datetime.now(timezone.utc),
                    }
                ]
            }
            mock_aws_clients[
                'applicationsignals_client'
            ].list_service_level_objectives.return_value = mock_slo_response

            # Mock metric data response showing breach
            mock_metric_response = {
                'MetricDataResults': [
                    {
                        'Id': 'slo0',
                        'Timestamps': [datetime.now(timezone.utc)],
                        'Values': [1.0],  # Breach count > 0 indicates breach
                    }
                ]
            }
            mock_aws_clients[
                'cloudwatch_client'
            ].get_metric_data.return_value = mock_metric_response

            result = await list_slis(hours=24)

            assert 'SLI Status Report - Last 24 hours' in result
            assert 'Transaction Search: ENABLED' in result
            assert 'BREACHED SERVICES:' in result
            assert 'test-service' in result


def test_check_transaction_search_enabled(mock_aws_clients):
    """Test checking transaction search status."""
    mock_aws_clients['xray_client'].get_trace_segment_destination.return_value = {
        'Destination': 'CloudWatchLogs',
        'Status': 'ACTIVE',
    }

    is_enabled, destination, status = check_transaction_search_enabled()

    assert is_enabled is True
    assert destination == 'CloudWatchLogs'
    assert status == 'ACTIVE'


def test_check_transaction_search_enabled_not_active(mock_aws_clients):
    """Test checking transaction search when not active."""
    mock_aws_clients['xray_client'].get_trace_segment_destination.return_value = {
        'Destination': 'XRay',
        'Status': 'INACTIVE',
    }

    is_enabled, destination, status = check_transaction_search_enabled()

    assert is_enabled is False
    assert destination == 'XRay'
    assert status == 'INACTIVE'


def test_check_transaction_search_enabled_error(mock_aws_clients):
    """Test checking transaction search with error."""
    mock_aws_clients['xray_client'].get_trace_segment_destination.side_effect = Exception(
        'API Error'
    )

    is_enabled, destination, status = check_transaction_search_enabled()

    assert is_enabled is False
    assert destination == 'Unknown'
    assert status == 'Error'


def test_remove_null_values():
    """Test remove_null_values function."""
    # Test with mix of None and non-None values
    input_dict = {
        'key1': 'value1',
        'key2': None,
        'key3': 'value3',
        'key4': None,
        'key5': 0,  # Should not be removed
        'key6': '',  # Should not be removed
        'key7': False,  # Should not be removed
    }

    result = remove_null_values(input_dict)

    assert result == {
        'key1': 'value1',
        'key3': 'value3',
        'key5': 0,
        'key6': '',
        'key7': False,
    }
    assert 'key2' not in result
    assert 'key4' not in result


@pytest.mark.asyncio
async def test_list_monitored_services_client_error(mock_aws_clients):
    """Test ClientError handling in list_monitored_services."""
    mock_aws_clients['applicationsignals_client'].list_services.side_effect = ClientError(
        error_response={
            'Error': {
                'Code': 'AccessDeniedException',
                'Message': 'User is not authorized to perform this action',
            }
        },
        operation_name='ListServices',
    )

    result = await list_monitored_services()

    assert 'AWS Error: User is not authorized to perform this action' in result


@pytest.mark.asyncio
async def test_list_monitored_services_general_exception(mock_aws_clients):
    """Test general exception handling in list_monitored_services."""
    mock_aws_clients['applicationsignals_client'].list_services.side_effect = Exception(
        'Unexpected error occurred'
    )

    result = await list_monitored_services()

    assert 'Error: Unexpected error occurred' in result


@pytest.mark.asyncio
async def test_query_service_metrics_no_datapoints(mock_aws_clients):
    """Test query service metrics when no datapoints are returned."""
    mock_list_response = {
        'ServiceSummaries': [
            {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
        ]
    }

    mock_get_response = {
        'Service': {
            'MetricReferences': [
                {
                    'Namespace': 'AWS/ApplicationSignals',
                    'MetricName': 'Latency',
                    'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
                }
            ]
        }
    }

    mock_metric_response = {'Datapoints': []}

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_list_response
    mock_aws_clients['applicationsignals_client'].get_service.return_value = mock_get_response
    mock_aws_clients['cloudwatch_client'].get_metric_statistics.return_value = mock_metric_response

    result = await query_service_metrics(
        service_name='test-service',
        metric_name='Latency',
        statistic='Average',
        extended_statistic='p99',
        hours=1,
    )

    assert 'No data points found' in result


@pytest.mark.asyncio
async def test_search_transaction_spans_timeout(mock_aws_clients):
    """Test search transaction spans with timeout."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.timer'
        ) as mock_timer:
            # Mock asyncio.sleep to prevent actual waiting
            with patch('asyncio.sleep', new_callable=AsyncMock):
                mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
                mock_aws_clients['logs_client'].start_query.return_value = {
                    'queryId': 'test-query-id'
                }
                mock_aws_clients['logs_client'].get_query_results.return_value = {
                    'status': 'Running'
                }

                # Simulate timeout by making timer exceed max_timeout
                mock_timer.side_effect = [
                    0,
                    0,
                    0,
                    31,
                    31,
                ]  # start_time_perf, poll_start, poll check 1, poll check 2

                result = await search_transaction_spans(
                    log_group_name='',
                    start_time='2024-01-01T00:00:00+00:00',
                    end_time='2024-01-01T01:00:00+00:00',
                    query_string='fields @timestamp',
                    limit=None,
                    max_timeout=30,
                )

                assert result['status'] == 'Polling Timeout'
                assert 'did not complete within 30 seconds' in result['message']


def test_main_normal_execution(mock_mcp):
    """Test normal execution of main function."""
    main()
    mock_mcp.run.assert_called_once_with(transport='stdio')


def test_main_keyboard_interrupt(mock_mcp):
    """Test KeyboardInterrupt handling in main function."""
    mock_mcp.run.side_effect = KeyboardInterrupt()
    # Should not raise an exception
    main()
    mock_mcp.run.assert_called_once_with(transport='stdio')


def test_main_general_exception(mock_mcp):
    """Test general exception handling in main function."""
    mock_mcp.run.side_effect = Exception('Server error')
    with pytest.raises(Exception, match='Server error'):
        main()
    mock_mcp.run.assert_called_once_with(transport='stdio')


@pytest.mark.asyncio
async def test_get_slo_period_based(mock_aws_clients):
    """Test get_slo with period-based SLI configuration."""
    mock_slo_response = {
        'Slo': {
            'Name': 'test-slo-period',
            'Arn': 'arn:aws:application-signals:us-east-1:123456789012:slo/test-slo',
            'EvaluationType': 'PERIOD_BASED',
            'Sli': {
                'SliMetric': {
                    'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::Lambda::Function'},
                    'OperationName': 'ProcessOrder',
                    'MetricType': 'AVAILABILITY',
                    'DependencyConfig': {
                        'DependencyKeyAttributes': {'Name': 'payment-service'},
                        'DependencyOperationName': 'ProcessPayment',
                    },
                },
                'MetricThreshold': 0.99,
                'ComparisonOperator': 'LessThan',
            },
            'BurnRateConfigurations': [
                {'LookBackWindowMinutes': 5},
                {'LookBackWindowMinutes': 60},
            ],
        }
    }

    mock_aws_clients[
        'applicationsignals_client'
    ].get_service_level_objective.return_value = mock_slo_response

    result = await get_slo('test-slo-period')

    assert 'PERIOD_BASED' in result
    assert 'ProcessOrder' in result
    assert 'annotation[aws.remote.operation]="ProcessPayment"' in result
    assert 'Burn Rate Configurations' in result


@pytest.mark.asyncio
async def test_list_slis_with_error_in_sli_client(mock_aws_clients):
    """Test list_slis when SLIReportClient throws error for some services."""
    mock_services_response = {
        'ServiceSummaries': [
            {
                'KeyAttributes': {
                    'Name': 'test-service-1',
                    'Type': 'AWS::ECS::Service',
                    'Environment': 'production',
                }
            },
            {
                'KeyAttributes': {
                    'Name': 'test-service-2',
                    'Type': 'AWS::Lambda::Function',
                    'Environment': 'staging',
                }
            },
        ]
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.SLIReportClient'
    ) as mock_sli_client:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
        ) as mock_check:
            mock_aws_clients[
                'applicationsignals_client'
            ].list_services.return_value = mock_services_response
            mock_check.return_value = (False, 'XRay', 'INACTIVE')

            # First service succeeds, second fails
            mock_report = MagicMock()
            mock_report.breached_slo_count = 0
            mock_report.ok_slo_count = 3
            mock_report.total_slo_count = 3
            mock_report.sli_status = 'OK'
            mock_report.start_time = datetime.now(timezone.utc) - timedelta(hours=24)
            mock_report.end_time = datetime.now(timezone.utc)

            mock_sli_client.return_value.generate_sli_report.side_effect = [
                mock_report,
                Exception('Failed to get SLI report'),
            ]

            result = await list_slis(hours=24)

            assert 'Transaction Search: NOT ENABLED' in result
            assert 'HEALTHY SERVICES:' in result
            assert 'INSUFFICIENT DATA:' in result
            assert 'test-service-1' in result
            assert 'test-service-2' in result


@pytest.mark.asyncio
async def test_query_service_metrics_service_not_found(mock_aws_clients):
    """Test query service metrics when service is not found."""
    mock_list_response = {'ServiceSummaries': []}

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_list_response

    result = await query_service_metrics(
        service_name='nonexistent-service',
        metric_name='Latency',
        statistic='Average',
        extended_statistic='p99',
        hours=1,
    )

    assert "Service 'nonexistent-service' not found" in result


@pytest.mark.asyncio
async def test_query_service_metrics_no_metrics(mock_aws_clients):
    """Test query service metrics when service has no metrics."""
    mock_list_response = {
        'ServiceSummaries': [
            {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
        ]
    }

    mock_get_response = {'Service': {'MetricReferences': []}}

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_list_response
    mock_aws_clients['applicationsignals_client'].get_service.return_value = mock_get_response

    result = await query_service_metrics(
        service_name='test-service',
        metric_name='Latency',
        statistic='Average',
        extended_statistic='p99',
        hours=1,
    )

    assert "No metrics found for service 'test-service'" in result


@pytest.mark.asyncio
async def test_query_service_metrics_metric_not_found(mock_aws_clients):
    """Test query service metrics when specific metric is not found."""
    mock_list_response = {
        'ServiceSummaries': [
            {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
        ]
    }

    mock_get_response = {
        'Service': {
            'MetricReferences': [
                {
                    'Namespace': 'AWS/ApplicationSignals',
                    'MetricName': 'Error',
                    'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
                }
            ]
        }
    }

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_list_response
    mock_aws_clients['applicationsignals_client'].get_service.return_value = mock_get_response

    result = await query_service_metrics(
        service_name='test-service',
        metric_name='Latency',  # Looking for Latency but only Error exists
        statistic='Average',
        extended_statistic='p99',
        hours=1,
    )

    assert "Metric 'Latency' not found" in result
    assert 'Available: Error' in result


@pytest.mark.asyncio
async def test_query_service_metrics_client_error(mock_aws_clients):
    """Test query service metrics with client error."""
    mock_aws_clients['applicationsignals_client'].list_services.side_effect = ClientError(
        error_response={
            'Error': {
                'Code': 'AccessDeniedException',
                'Message': 'User is not authorized',
            }
        },
        operation_name='ListServices',
    )

    result = await query_service_metrics(
        service_name='test-service',
        metric_name='Latency',
        statistic='Average',
        extended_statistic='p99',
        hours=1,
    )

    assert 'AWS Error: User is not authorized' in result


@pytest.mark.asyncio
async def test_search_transaction_spans_failed_query(mock_aws_clients):
    """Test search transaction spans when query fails."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Failed',
            'statistics': {'error': 'Query syntax error'},
        }

        result = await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='invalid query',
            limit=None,
            max_timeout=30,
        )

        assert result['status'] == 'Failed'


@pytest.mark.asyncio
async def test_get_slo_client_error(mock_aws_clients):
    """Test get_slo with client error."""
    mock_aws_clients[
        'applicationsignals_client'
    ].get_service_level_objective.side_effect = ClientError(
        error_response={
            'Error': {
                'Code': 'ResourceNotFoundException',
                'Message': 'SLO not found',
            }
        },
        operation_name='GetServiceLevelObjective',
    )

    result = await get_slo('test-slo-id')

    assert 'AWS Error: SLO not found' in result


@pytest.mark.asyncio
async def test_search_transaction_spans_empty_log_group(mock_aws_clients):
    """Empty log_group_name triggers SOURCE logGroups() + @data_format filterIndex."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp',
            limit=None,
            max_timeout=30,
        )

        mock_aws_clients['logs_client'].start_query.assert_called()
        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        assert 'logGroupNames' not in call_args
        assert 'SOURCE logGroups()' in call_args['queryString']
        assert 'filterIndex @data_format = "AWS-OTEL-TRACE-V1"' in call_args['queryString']
        assert 'fields @timestamp' in call_args['queryString']


@pytest.mark.asyncio
async def test_list_slis_no_services(mock_aws_clients):
    """Test list_slis when no services exist."""
    mock_aws_clients['applicationsignals_client'].list_services.return_value = {
        'ServiceSummaries': []
    }

    result = await list_slis(hours=24)

    assert 'No services found in Application Signals.' in result


@pytest.mark.asyncio
async def test_get_slo_with_calendar_interval(mock_aws_clients):
    """Test get_slo with calendar interval in goal."""
    mock_slo_response = {
        'Slo': {
            'Name': 'test-slo-calendar',
            'Goal': {
                'AttainmentGoal': 99.5,
                'Interval': {
                    'CalendarInterval': {
                        'Duration': 1,
                        'DurationUnit': 'MONTH',
                        'StartTime': '2024-01-01T00:00:00Z',
                    }
                },
            },
        }
    }

    mock_aws_clients[
        'applicationsignals_client'
    ].get_service_level_objective.return_value = mock_slo_response

    result = await get_slo('test-slo-calendar')

    assert 'Calendar 1 MONTH starting 2024-01-01T00:00:00Z' in result


@pytest.mark.asyncio
async def test_query_service_metrics_different_periods(mock_aws_clients):
    """Test query service metrics with different time periods."""
    # Test data for different hour ranges
    test_cases = [
        (2, 60),  # 2 hours -> 1 minute period
        (12, 300),  # 12 hours -> 5 minute period
        (48, 3600),  # 48 hours -> 1 hour period
    ]

    for hours, expected_period in test_cases:
        mock_list_response = {
            'ServiceSummaries': [
                {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
            ]
        }

        mock_get_response = {
            'Service': {
                'MetricReferences': [
                    {
                        'Namespace': 'AWS/ApplicationSignals',
                        'MetricName': 'Latency',
                        'Dimensions': [],
                    }
                ]
            }
        }

        mock_metric_response = {
            'Datapoints': [{'Timestamp': datetime.now(timezone.utc), 'Average': 100.0}]
        }

        mock_aws_clients[
            'applicationsignals_client'
        ].list_services.return_value = mock_list_response
        mock_aws_clients['applicationsignals_client'].get_service.return_value = mock_get_response
        mock_aws_clients[
            'cloudwatch_client'
        ].get_metric_statistics.return_value = mock_metric_response

        await query_service_metrics(
            service_name='test-service',
            metric_name='Latency',
            statistic='Average',
            extended_statistic='p99',
            hours=hours,
        )

        # Verify the period was set correctly
        call_args = mock_aws_clients['cloudwatch_client'].get_metric_statistics.call_args[1]
        assert call_args['Period'] == expected_period


@pytest.mark.asyncio
async def test_query_service_metrics_general_exception(mock_aws_clients):
    """Test query service metrics with unexpected exception."""
    mock_aws_clients['applicationsignals_client'].list_services.side_effect = Exception(
        'Unexpected error'
    )

    result = await query_service_metrics(
        service_name='test-service',
        metric_name='Latency',
        statistic='Average',
        extended_statistic='p99',
        hours=1,
    )

    assert 'Error: Unexpected error' in result


@pytest.mark.asyncio
async def test_search_transaction_spans_general_exception(mock_aws_clients):
    """Test search transaction spans with general exception."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.side_effect = Exception('Query failed')

        with pytest.raises(Exception) as exc_info:
            await search_transaction_spans(
                log_group_name='aws/spans',
                start_time='2024-01-01T00:00:00+00:00',
                end_time='2024-01-01T01:00:00+00:00',
                query_string='fields @timestamp',
                limit=100,
                max_timeout=30,
            )

        assert 'Query failed' in str(exc_info.value)


@pytest.mark.asyncio
async def test_list_monitored_services_with_attributes_branch(mock_aws_clients):
    """Test list_monitored_services with key attributes that trigger the branch."""
    mock_response = {
        'ServiceSummaries': [
            {
                'KeyAttributes': {}  # Empty attributes to test the branch
            }
        ]
    }

    mock_aws_clients['applicationsignals_client'].list_services.return_value = mock_response

    result = await list_monitored_services()

    assert 'Application Signals Services (1 total' in result
    assert 'Key Attributes:' not in result  # Should not show when empty


@pytest.mark.asyncio
async def test_get_slo_with_period_based_sli_full_details(mock_aws_clients):
    """Test get_slo with comprehensive period-based SLI configuration."""
    mock_response = {
        'Slo': {
            'Name': 'test-slo',
            'Arn': 'arn:aws:slo:test',
            'Description': 'Test SLO',
            'EvaluationType': 'PERIOD_BASED',
            'CreatedTime': datetime.now(timezone.utc),
            'LastUpdatedTime': datetime.now(timezone.utc),
            'Goal': {
                'AttainmentGoal': 99.9,
                'WarningThreshold': 95,
                'Interval': {
                    'CalendarInterval': {
                        'Duration': 1,
                        'DurationUnit': 'MONTH',
                        'StartTime': datetime.now(timezone.utc),
                    }
                },
            },
            'Sli': {
                'SliMetric': {
                    'KeyAttributes': {'Service': 'test-service', 'Environment': 'prod'},
                    'OperationName': 'GetItem',
                    'MetricType': 'LATENCY',
                    'MetricDataQueries': [
                        {
                            'Id': 'query1',
                            'MetricStat': {
                                'Metric': {
                                    'Namespace': 'AWS/ApplicationSignals',
                                    'MetricName': 'Latency',
                                    'Dimensions': [
                                        {'Name': 'Service', 'Value': 'test-service'},
                                        {'Name': 'Operation', 'Value': 'GetItem'},
                                    ],
                                },
                                'Period': 300,
                                'Stat': 'p99',
                                'Unit': 'Milliseconds',
                            },
                            'ReturnData': True,
                        },
                        {'Id': 'query2', 'Expression': 'query1 * 2', 'ReturnData': False},
                    ],
                    'DependencyConfig': {
                        'DependencyKeyAttributes': {
                            'RemoteService': 'downstream-service',
                            'RemoteEnvironment': 'prod',
                        },
                        'DependencyOperationName': 'ProcessRequest',
                    },
                },
                'MetricThreshold': 1000,
                'ComparisonOperator': 'LessThan',
            },
            'BurnRateConfigurations': [
                {'LookBackWindowMinutes': 5},
                {'LookBackWindowMinutes': 60},
            ],
        }
    }

    mock_aws_clients[
        'applicationsignals_client'
    ].get_service_level_objective.return_value = mock_response

    result = await get_slo('test-slo-id')

    # Verify all sections are present
    assert 'Service Level Objective Details' in result
    assert 'Goal Configuration' in result
    assert 'Calendar 1 MONTH' in result
    assert 'Period-Based SLI Configuration' in result
    assert 'Key Attributes:' in result
    assert 'Service: test-service' in result
    assert 'Operation Name: GetItem' in result
    assert 'Metric Data Queries:' in result
    assert 'Query ID: query1' in result
    assert 'Namespace: AWS/ApplicationSignals' in result
    assert 'Dimensions:' in result
    assert 'Expression: query1 * 2' in result
    assert 'ReturnData: False' in result
    assert 'Dependency Configuration:' in result
    assert 'RemoteService: downstream-service' in result
    assert 'Dependency Operation: ProcessRequest' in result
    assert 'Burn Rate Configurations:' in result


@pytest.mark.asyncio
async def test_get_slo_with_request_based_sli_full_details(mock_aws_clients):
    """Test get_slo with comprehensive request-based SLI configuration."""
    mock_response = {
        'Slo': {
            'Name': 'test-slo-rbs',
            'Arn': 'arn:aws:slo:test-rbs',
            'Goal': {
                'AttainmentGoal': 99.5,
                'Interval': {'RollingInterval': {'Duration': 7, 'DurationUnit': 'DAY'}},
            },
            'RequestBasedSli': {
                'RequestBasedSliMetric': {
                    'KeyAttributes': {'Service': 'api-service', 'Type': 'AWS::Lambda::Function'},
                    'OperationName': 'ProcessOrder',
                    'MetricType': 'AVAILABILITY',
                    'MetricDataQueries': [
                        {
                            'Id': 'success',
                            'MetricStat': {
                                'Metric': {
                                    'Namespace': 'AWS/Lambda',
                                    'MetricName': 'Success',
                                    'Dimensions': [
                                        {'Name': 'FunctionName', 'Value': 'process-order'}
                                    ],
                                },
                                'Period': 60,
                                'Stat': 'Sum',
                            },
                        },
                        {
                            'Id': 'errors',
                            'MetricStat': {
                                'Metric': {
                                    'Namespace': 'AWS/Lambda',
                                    'MetricName': 'Errors',
                                    'Dimensions': [
                                        {'Name': 'FunctionName', 'Value': 'process-order'}
                                    ],
                                },
                                'Period': 60,
                                'Stat': 'Sum',
                                'Unit': 'Count',
                            },
                        },
                        {'Id': 'availability', 'Expression': 'success / (success + errors) * 100'},
                    ],
                    'DependencyConfig': {
                        'DependencyKeyAttributes': {'Database': 'orders-db'},
                        'DependencyOperationName': 'Query',
                    },
                },
                'MetricThreshold': 99.0,
                'ComparisonOperator': 'GreaterThan',
            },
        }
    }

    mock_aws_clients[
        'applicationsignals_client'
    ].get_service_level_objective.return_value = mock_response

    result = await get_slo('test-slo-rbs-id')

    # Verify request-based sections
    assert 'Request-Based SLI Configuration:' in result
    assert 'api-service' in result
    assert 'ProcessOrder' in result
    assert 'AVAILABILITY' in result
    assert 'Expression: success / (success + errors) * 100' in result
    assert 'Dependency Configuration:' in result
    assert 'Database: orders-db' in result
    assert 'Unit: Count' in result


@pytest.mark.asyncio
async def test_get_slo_general_exception(mock_aws_clients):
    """Test get_slo with general exception."""
    mock_aws_clients[
        'applicationsignals_client'
    ].get_service_level_objective.side_effect = Exception('Unexpected error')

    result = await get_slo('test-slo-id')

    assert 'Error: Unexpected error' in result


@pytest.mark.asyncio
async def test_search_transaction_spans_with_none_log_group(mock_aws_clients):
    """None log_group_name is treated the same as empty: cross-log-group index query."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name=None,  # type: ignore
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp',
            limit=100,
            max_timeout=30,
        )

        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        assert 'logGroupNames' not in call_args
        assert 'SOURCE logGroups()' in call_args['queryString']
        assert 'filterIndex @data_format = "AWS-OTEL-TRACE-V1"' in call_args['queryString']


@pytest.mark.asyncio
async def test_search_transaction_spans_explicit_log_group_injects_filter_index(
    mock_aws_clients,
):
    """Explicit log_group_name scopes via logGroupNames and still injects the @data_format filterIndex."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name='custom-spans-lg',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp',
            limit=None,
            max_timeout=30,
        )

        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        assert call_args['logGroupNames'] == ['custom-spans-lg']
        assert 'SOURCE logGroups()' not in call_args['queryString']
        assert 'filterIndex @data_format = "AWS-OTEL-TRACE-V1"' in call_args['queryString']


@pytest.mark.asyncio
async def test_search_transaction_spans_user_source_clause_left_alone(mock_aws_clients):
    """A user-supplied SOURCE clause suppresses all automatic injection."""
    user_query = (
        'SOURCE logGroups(namePrefix: ["my/spans"]) '
        '| filterIndex @data_format = "AWS-OTEL-TRACE-V1" '
        '| fields @timestamp'
    )
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string=user_query,
            limit=None,
            max_timeout=30,
        )

        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        assert call_args['queryString'] == user_query
        # Exactly one SOURCE clause: the user's.
        assert call_args['queryString'].lower().count('source ') == 1


@pytest.mark.asyncio
async def test_search_transaction_spans_explicit_log_group_with_user_data_format(
    mock_aws_clients,
):
    """Explicit log_group_name + user-supplied @data_format: no injection at all."""
    user_query = 'filter @data_format = "AWS-OTEL-TRACE-V1" | fields @timestamp'
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name='custom-spans-lg',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string=user_query,
            limit=None,
            max_timeout=30,
        )

        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        assert call_args['logGroupNames'] == ['custom-spans-lg']
        # Neither SOURCE nor a second filterIndex should be prepended.
        assert call_args['queryString'] == user_query


@pytest.mark.asyncio
async def test_search_transaction_spans_user_source_wins_over_log_group_name(
    mock_aws_clients,
):
    """If the user supplies a SOURCE clause, log_group_name is dropped (not both)."""
    user_query = (
        'SOURCE logGroups(namePrefix: ["my/spans"]) '
        '| filterIndex @data_format = "AWS-OTEL-TRACE-V1" '
        '| fields @timestamp'
    )
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name='ignored-log-group',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string=user_query,
            limit=None,
            max_timeout=30,
        )

        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        assert call_args['queryString'] == user_query
        # logGroupNames must NOT be sent when SOURCE is present — CloudWatch rejects
        # that combination.
        assert 'logGroupNames' not in call_args


@pytest.mark.asyncio
async def test_search_transaction_spans_user_data_format_filter_preserved(mock_aws_clients):
    """If the user already references @data_format, the tool does not double-inject the filter."""
    user_query = 'filter @data_format = "AWS-VENDED-LOG-V1" | fields @timestamp'
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string=user_query,
            limit=None,
            max_timeout=30,
        )

        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        # SOURCE logGroups() still added (no explicit log group).
        assert 'SOURCE logGroups()' in call_args['queryString']
        # But the AWS-OTEL-TRACE-V1 filter is NOT injected — user's @data_format filter wins.
        assert 'AWS-OTEL-TRACE-V1' not in call_args['queryString']
        assert 'AWS-VENDED-LOG-V1' in call_args['queryString']


def test_compose_spans_query_never_ends_with_pipe():
    """Guard against regressions of the empty-query trailing-pipe bug."""
    # Valid inputs: final query must not end with '|' or '| '.
    for q in ('fields @timestamp', '   fields @timestamp   ', 'filter x = "y"'):
        for lg in ('', 'my-lg'):
            out = _compose_spans_query(q, lg)
            assert not out.rstrip().endswith('|'), f'trailing pipe for q={q!r} lg={lg!r}: {out!r}'
    # Empty / whitespace-only queries: helper must not emit trailing pipes even
    # though the tool entry point rejects these before reaching the helper.
    for q in ('', '   '):
        out = _compose_spans_query(q, '')
        assert not out.rstrip().endswith('|'), f'trailing pipe for empty q={q!r}: {out!r}'


@pytest.mark.asyncio
async def test_search_transaction_spans_empty_query_string_rejected(mock_aws_clients):
    """Empty query_string returns an Invalid Input error without calling StartQuery."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')

        result = await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='',
            limit=None,
            max_timeout=30,
        )

        assert result['status'] == 'Invalid Input'
        assert 'query_string' in result['message']
        mock_aws_clients['logs_client'].start_query.assert_not_called()


@pytest.mark.asyncio
async def test_search_transaction_spans_whitespace_query_string_rejected(mock_aws_clients):
    """Whitespace-only query_string also returns Invalid Input (not a bare SOURCE that would error in CWL)."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')

        result = await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='   \n\t  ',
            limit=None,
            max_timeout=30,
        )

        assert result['status'] == 'Invalid Input'
        mock_aws_clients['logs_client'].start_query.assert_not_called()


@pytest.mark.asyncio
async def test_search_transaction_spans_log_group_name_ignored_surfaced_in_response(
    mock_aws_clients,
):
    """When log_group_name is dropped because of a user SOURCE clause, the response says so."""
    user_query = (
        'SOURCE logGroups(namePrefix: ["my/spans"]) '
        '| filterIndex @data_format = "AWS-OTEL-TRACE-V1" '
        '| fields @timestamp'
    )
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'statistics': {'recordsMatched': 0},
            'results': [],
        }

        result = await search_transaction_spans(
            log_group_name='ignored-log-group',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string=user_query,
            limit=None,
            max_timeout=30,
        )

        assert result['log_group_name_ignored'] is True
        assert 'SOURCE' in result['log_group_name_ignored_reason']


@pytest.mark.asyncio
async def test_search_transaction_spans_log_group_name_ignored_absent_in_normal_response(
    mock_aws_clients,
):
    """The ignored flag must NOT leak into responses where log_group_name was actually used."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'statistics': {'recordsMatched': 0},
            'results': [],
        }

        result = await search_transaction_spans(
            log_group_name='my-lg',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp',
            limit=None,
            max_timeout=30,
        )

        assert 'log_group_name_ignored' not in result


@pytest.mark.asyncio
async def test_search_transaction_spans_log_group_name_ignored_surfaced_on_timeout(
    mock_aws_clients,
):
    """On Polling Timeout, the ignored flag and reason are still surfaced in the response."""
    user_query = 'SOURCE logGroups(namePrefix: ["my/spans"]) | fields @timestamp'
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.timer'
        ) as mock_timer:
            with patch('asyncio.sleep', new_callable=AsyncMock):
                mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
                mock_aws_clients['logs_client'].start_query.return_value = {
                    'queryId': 'test-query-id'
                }
                mock_aws_clients['logs_client'].get_query_results.return_value = {
                    'status': 'Running'
                }
                mock_timer.side_effect = [0, 0, 0, 31, 31]

                result = await search_transaction_spans(
                    log_group_name='ignored-log-group',
                    start_time='2024-01-01T00:00:00+00:00',
                    end_time='2024-01-01T01:00:00+00:00',
                    query_string=user_query,
                    limit=None,
                    max_timeout=30,
                )

                assert result['status'] == 'Polling Timeout'
                assert result['log_group_name_ignored'] is True
                assert 'SOURCE' in result['log_group_name_ignored_reason']


@pytest.mark.asyncio
async def test_search_transaction_spans_injection_not_confused_by_data_format_prefix(
    mock_aws_clients,
):
    """A field like @data_format_version should NOT suppress the @data_format filterIndex."""
    user_query = 'fields @data_format_version, @timestamp'
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string=user_query,
            limit=None,
            max_timeout=30,
        )

        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        assert 'filterIndex @data_format = "AWS-OTEL-TRACE-V1"' in call_args['queryString']


@pytest.mark.asyncio
async def test_search_transaction_spans_source_check_not_confused_by_identifier_prefix(
    mock_aws_clients,
):
    """A leading token like `sourced` must not be mistaken for a SOURCE clause."""
    # `sourced` isn't a real CWLI keyword, but it's a worst-case prefix collision test.
    user_query = 'sourced_spans | fields @timestamp'
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'results': [],
        }

        await search_transaction_spans(
            log_group_name='',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string=user_query,
            limit=None,
            max_timeout=30,
        )

        call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
        # Because the token isn't actually SOURCE, both injections should fire.
        assert 'SOURCE logGroups()' in call_args['queryString']
        assert 'filterIndex @data_format = "AWS-OTEL-TRACE-V1"' in call_args['queryString']


@pytest.mark.asyncio
async def test_search_transaction_spans_complete_with_statistics(mock_aws_clients):
    """Test search_transaction_spans when query completes with detailed statistics."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}

        # First return Running, then Complete
        mock_aws_clients['logs_client'].get_query_results.side_effect = [
            {'queryId': 'test-query-id', 'status': 'Running'},
            {
                'queryId': 'test-query-id',
                'status': 'Complete',
                'statistics': {
                    'recordsMatched': 100,
                    'recordsScanned': 1000,
                    'bytesScanned': 50000,
                },
                'results': [
                    [
                        {'field': 'spanId', 'value': 'span1'},
                        {'field': '@timestamp', 'value': '2024-01-01 00:00:00'},
                    ]
                ],
            },
        ]

        result = await search_transaction_spans(
            log_group_name='aws/spans',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp, spanId',
            limit=100,
            max_timeout=30,
        )

        assert result['status'] == 'Complete'
        assert result['statistics']['recordsMatched'] == 100
        assert len(result['results']) == 1


@pytest.mark.asyncio
async def test_search_transaction_spans_code_level_attributes_detected(mock_aws_clients):
    """Test search_transaction_spans when code-level attributes are detected in results."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'statistics': {'recordsMatched': 2},
            'results': [
                [
                    {'field': 'spanId', 'value': 'span1'},
                    {'field': 'attributes.code.file.path', 'value': '/app/src/handler.py'},
                    {'field': 'attributes.code.function.name', 'value': 'process_request'},
                    {'field': 'attributes.code.line.number', 'value': '42'},
                ]
            ],
        }

        result = await search_transaction_spans(
            log_group_name='aws/spans',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp, spanId, attributes.code.file.path',
            limit=100,
            max_timeout=30,
        )

        assert result['status'] == 'Complete'
        assert 'code_level_attributes_status' in result
        assert result['code_level_attributes_status']['detected'] is True
        assert 'code.file.path' in result['code_level_attributes_status']['attributes_found']
        assert 'code.function.name' in result['code_level_attributes_status']['attributes_found']
        assert 'code.line.number' in result['code_level_attributes_status']['attributes_found']
        assert result['code_level_attributes_status']['requested_in_query'] is True
        assert (
            '✅ Code-Level Attributes Available'
            in result['code_level_attributes_status']['message']
        )


@pytest.mark.asyncio
async def test_search_transaction_spans_code_level_attributes_requested_but_not_found(
    mock_aws_clients,
):
    """Test search_transaction_spans when code-level attributes are requested but not in results."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'statistics': {'recordsMatched': 1},
            'results': [
                [
                    {'field': 'spanId', 'value': 'span1'},
                    {'field': '@timestamp', 'value': '2024-01-01 00:00:00'},
                ]
            ],
        }

        result = await search_transaction_spans(
            log_group_name='aws/spans',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp, spanId, attributes.code.file.path',
            limit=100,
            max_timeout=30,
        )

        assert result['status'] == 'Complete'
        assert 'code_level_attributes_status' in result
        assert result['code_level_attributes_status']['detected'] is False
        assert result['code_level_attributes_status']['requested_in_query'] is True
        assert (
            'Code-level attributes not available'
            in result['code_level_attributes_status']['message']
        )
        assert (
            'OTEL_AWS_EXPERIMENTAL_CODE_ATTRIBUTES=true'
            in result['code_level_attributes_status']['message']
        )
        assert 'suggestion' in result['code_level_attributes_status']


@pytest.mark.asyncio
async def test_search_transaction_spans_code_level_attributes_not_requested(mock_aws_clients):
    """Test search_transaction_spans when code-level attributes are not requested."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'statistics': {'recordsMatched': 1},
            'results': [
                [
                    {'field': 'spanId', 'value': 'span1'},
                    {'field': '@timestamp', 'value': '2024-01-01 00:00:00'},
                ]
            ],
        }

        result = await search_transaction_spans(
            log_group_name='aws/spans',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields @timestamp, spanId',
            limit=100,
            max_timeout=30,
        )

        assert result['status'] == 'Complete'
        assert 'code_level_attributes_status' in result
        assert result['code_level_attributes_status']['detected'] is False
        assert result['code_level_attributes_status']['requested_in_query'] is False
        # Should not have message or suggestion when not requested
        assert 'suggestion' not in result['code_level_attributes_status']


@pytest.mark.asyncio
async def test_search_transaction_spans_code_level_attributes_without_prefix(mock_aws_clients):
    """Test search_transaction_spans with code-level attributes without 'attributes.' prefix."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'statistics': {'recordsMatched': 1},
            'results': [
                [
                    {'field': 'spanId', 'value': 'span1'},
                    {'field': 'code.file.path', 'value': '/app/src/main.py'},
                    {'field': 'code.function.name', 'value': 'main'},
                ]
            ],
        }

        result = await search_transaction_spans(
            log_group_name='aws/spans',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields spanId, code.file.path, code.function.name',
            limit=100,
            max_timeout=30,
        )

        assert result['status'] == 'Complete'
        assert 'code_level_attributes_status' in result
        assert result['code_level_attributes_status']['detected'] is True
        assert 'code.file.path' in result['code_level_attributes_status']['attributes_found']
        assert 'code.function.name' in result['code_level_attributes_status']['attributes_found']
        assert result['code_level_attributes_status']['requested_in_query'] is True


@pytest.mark.asyncio
async def test_search_transaction_spans_code_level_attributes_mixed_results(mock_aws_clients):
    """Test search_transaction_spans with mixed results (some with code-level attributes, some without)."""
    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.trace_tools.check_transaction_search_enabled'
    ) as mock_check:
        mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
        mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
        mock_aws_clients['logs_client'].get_query_results.return_value = {
            'queryId': 'test-query-id',
            'status': 'Complete',
            'statistics': {'recordsMatched': 3},
            'results': [
                [
                    {'field': 'spanId', 'value': 'span1'},
                    {'field': 'attributes.code.file.path', 'value': '/app/handler.py'},
                ],
                [
                    {'field': 'spanId', 'value': 'span2'},
                    {'field': '@timestamp', 'value': '2024-01-01 00:00:00'},
                ],
                [
                    {'field': 'spanId', 'value': 'span3'},
                    {'field': 'code.line.number', 'value': '100'},
                ],
            ],
        }

        result = await search_transaction_spans(
            log_group_name='aws/spans',
            start_time='2024-01-01T00:00:00+00:00',
            end_time='2024-01-01T01:00:00+00:00',
            query_string='fields spanId, `attributes.code.file.path`, `code.line.number`',
            limit=100,
            max_timeout=30,
        )

        assert result['status'] == 'Complete'
        assert 'code_level_attributes_status' in result
        assert result['code_level_attributes_status']['detected'] is True
        # Should detect both attributes found across different results
        assert 'code.file.path' in result['code_level_attributes_status']['attributes_found']
        assert 'code.line.number' in result['code_level_attributes_status']['attributes_found']
        assert len(result['code_level_attributes_status']['attributes_found']) == 2


@pytest.mark.asyncio
async def test_list_slis_general_exception(mock_aws_clients):
    """Test list_slis with general exception."""
    mock_aws_clients['applicationsignals_client'].list_services.side_effect = Exception(
        'Service unavailable'
    )

    result = await list_slis(hours=24)

    assert 'Error getting SLI status: Service unavailable' in result


def test_main_success(mock_aws_clients):
    """Test main function normal execution."""
    with patch('awslabs.cloudwatch_applicationsignals_mcp_server.server.mcp') as mock_mcp:
        main()
        mock_mcp.run.assert_called_once_with(transport='stdio')


def test_main_exception(mock_aws_clients):
    """Test main function with general exception."""
    with patch('awslabs.cloudwatch_applicationsignals_mcp_server.server.mcp') as mock_mcp:
        mock_mcp.run.side_effect = Exception('Server error')

        with pytest.raises(Exception) as exc_info:
            main()

        assert 'Server error' in str(exc_info.value)


def test_main_entry_point(mock_aws_clients):
    """Test the if __name__ == '__main__' entry point."""
    # The __main__ block is simple and just calls main()
    # We can't easily test it without executing the module
    # So we'll just ensure the main() function works
    # The actual line 1346 will be covered when the module is imported
    # during normal test execution

    # Instead, let's just verify the main function exists and is callable
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import main

    assert callable(main)

    # And verify that running main with mocked mcp doesn't raise
    with patch('awslabs.cloudwatch_applicationsignals_mcp_server.server.mcp') as mock_mcp:
        mock_mcp.run.side_effect = KeyboardInterrupt()
        # Should handle KeyboardInterrupt gracefully
        main()


@pytest.mark.asyncio
async def test_analyze_canary_failures_no_runs(mock_aws_clients):
    """Test analyze_canary_failures when no runs are found."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': []}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {'Name': 'test-canary'}
    }

    result = await analyze_canary_failures('test-canary', 'us-east-1')

    assert 'No run history found for test-canary' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_healthy_canary(mock_aws_clients):
    """Test analyze_canary_failures with healthy canary."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'run1',
            'Status': {'State': 'PASSED'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {'Name': 'test-canary'}
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        mock_insights.return_value = 'Telemetry insights available'

        result = await analyze_canary_failures('test-canary', 'us-east-1')

        assert 'Canary is healthy - no failures since last success' in result
        assert '🔍 Comprehensive Failure Analysis for test-canary' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_telemetry_unavailable(mock_aws_clients):
    """Test analyze_canary_failures when telemetry is unavailable."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'run1',
            'Status': {'State': 'PASSED'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {'Name': 'test-canary'}
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        mock_insights.side_effect = Exception('Telemetry API error')

        result = await analyze_canary_failures('test-canary', 'us-east-1')

        assert 'Telemetry API unavailable: Telemetry API error' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_with_failures(mock_aws_clients):
    """Test analyze_canary_failures with actual failures."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run-1',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
            'Timeline': {'Started': '2024-01-01T01:00:00Z'},
        },
        {
            'Id': 'failed-run-2',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
            'Timeline': {'Started': '2024-01-01T00:30:00Z'},
        },
        {
            'Id': 'success-run',
            'Status': {'State': 'PASSED'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        },
    ]

    mock_canary = {
        'Name': 'test-canary',
        'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
    }

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    # Mock S3 artifacts
    mock_aws_clients['s3_client'].list_objects_v2.return_value = {
        'Contents': [
            {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
            {'Key': 'canary/us-east-1/test-canary/2024/01/01/screenshot.png'},
            {'Key': 'canary/us-east-1/test-canary/2024/01/01/logs.txt'},
        ]
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_har_file'
        ) as mock_har:
            with patch(
                'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_screenshots'
            ) as mock_screenshots:
                with patch(
                    'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_log_files'
                ) as mock_logs:
                    mock_insights.return_value = 'Telemetry insights'
                    mock_har.return_value = {
                        'failed_requests': 2,
                        'total_requests': 10,
                        'request_details': [
                            {'url': 'https://example.com', 'status': 500, 'time': 1000}
                        ],
                    }
                    mock_screenshots.return_value = {'insights': ['Screenshot analysis']}
                    mock_logs.return_value = {'insights': ['Log analysis']}

                    result = await analyze_canary_failures('test-canary', 'us-east-1')

                    assert 'Found 2 consecutive failures since last success' in result
                    assert 'All failures have same cause: Navigation timeout' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_iam_analysis(mock_aws_clients):
    """Test analyze_canary_failures with IAM-related failures."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Access denied'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_iam_role_and_policies'
        ) as mock_iam:
            with patch(
                'awslabs.cloudwatch_applicationsignals_mcp_server.server.check_resource_arns_correct'
            ) as mock_arn:
                mock_insights.return_value = 'Telemetry insights'
                mock_iam.return_value = {
                    'status': 'issues_found',
                    'checks': {'role_exists': 'PASS', 'policies_attached': 'FAIL'},
                    'issues_found': ['Missing S3 permissions'],
                    'recommendations': ['Add S3 read permissions'],
                }
                mock_arn.return_value = {
                    'correct': False,
                    'error': 'Invalid bucket ARN',
                    'issues': ['Bucket name mismatch'],
                }

                result = await analyze_canary_failures('test-canary', 'us-east-1')

                assert 'RUNNING COMPREHENSIVE IAM ANALYSIS' in result
                assert 'IAM Role Analysis Status: issues_found' in result
                assert 'ALL IAM ISSUES FOUND (2 total):' in result
                assert 'IAM Policy: Missing S3 permissions' in result
                assert 'Resource ARN: Bucket name mismatch' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_enospc_error(mock_aws_clients):
    """Test analyze_canary_failures with ENOSPC error."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'ENOSPC: no space left on device'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.extract_disk_memory_usage_metrics'
        ) as mock_metrics:
            mock_insights.return_value = 'Telemetry insights'
            mock_metrics.return_value = {
                'maxEphemeralStorageUsageInMb': 512.5,
                'maxEphemeralStorageUsagePercent': 95.2,
            }

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert 'DISK USAGE ROOT CAUSE ANALYSIS:' in result
            assert 'Storage: 512.5 MB peak' in result
            assert 'Usage: 95.2% peak' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_protocol_error(mock_aws_clients):
    """Test analyze_canary_failures with protocol error."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {
                'State': 'FAILED',
                'StateReason': 'Protocol error (Target.activateTarget): Session closed',
            },
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.extract_disk_memory_usage_metrics'
        ) as mock_metrics:
            mock_insights.return_value = 'Telemetry insights'
            mock_metrics.return_value = {'maxSyntheticsMemoryUsageInMB': 256.8}

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert 'MEMORY USAGE ROOT CAUSE ANALYSIS:' in result
            assert 'Memory: 256.8 MB peak' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_navigation_timeout_with_har(mock_aws_clients):
    """Test analyze_canary_failures with navigation timeout and HAR analysis."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timed out after 30000ms'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {
        'Name': 'test-canary',
        'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
    }

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    # Mock S3 to return HAR files
    mock_aws_clients['s3_client'].list_objects_v2.return_value = {
        'Contents': [
            {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
        ]
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_har_file'
        ) as mock_har:
            mock_insights.return_value = 'Telemetry insights'
            mock_har.return_value = {
                'failed_requests': 5,
                'total_requests': 10,
                'request_details': [
                    {'url': 'https://example.com/slow', 'status': 200, 'time': 5000}
                ],
                'insights': [
                    'Slow DNS resolution detected',
                    'High server response time',
                    'Network connectivity issues',
                    'Resource loading delays',
                    'JavaScript execution timeout',
                ],
            }

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert '🔍 Comprehensive Failure Analysis for test-canary' in result
            assert 'Slow DNS resolution detected' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_s3_exception(mock_aws_clients):
    """Test analyze_canary_failures when S3 operations fail."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {
        'Name': 'test-canary',
        'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
    }

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    mock_aws_clients['s3_client'].list_objects_v2.side_effect = Exception('S3 access denied')

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_canary_logs_with_time_window'
        ) as mock_logs:
            mock_insights.return_value = 'Telemetry insights'
            mock_logs.return_value = {
                'status': 'success',
                'time_window': '2024-01-01 00:00:00 - 2024-01-01 00:05:00',
                'total_events': 5,
                'error_events': [
                    {'timestamp': datetime.now(timezone.utc), 'message': 'Test error message'}
                ],
            }

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            # Should fall back to CloudWatch Logs analysis when S3 fails
            assert '⚠️ Artifacts not available - Checking CloudWatch Logs for root cause' in result
            assert 'CLOUDWATCH LOGS ANALYSIS' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_visual_variation(mock_aws_clients):
    """Test analyze_canary_failures with visual variation error."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Visual variation detected'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_code'
        ) as mock_code:
            mock_insights.return_value = 'Telemetry insights'
            mock_code.return_value = {'code_content': 'const synthetics = require("Synthetics");'}

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert 'VISUAL MONITORING ISSUE DETECTED' in result
            assert 'Website UI changed - not a technical failure' in result
            assert 'canary code:' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_get_canary_code_exception(mock_aws_clients):
    """Test analyze_canary_failures when get_canary_code fails."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_code'
        ) as mock_code:
            mock_insights.return_value = 'Telemetry insights'
            # Make get_canary_code raise an exception
            mock_code.side_effect = Exception('Code retrieval failed')

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert 'Note: Could not retrieve canary code: Code retrieval failed' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_iam_analysis_exception(mock_aws_clients):
    """Test analyze_canary_failures when IAM analysis fails."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Access denied'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_iam_role_and_policies'
        ) as mock_iam:
            mock_insights.return_value = 'Telemetry insights'
            # Make IAM analysis raise an exception
            mock_iam.side_effect = Exception('IAM analysis failed')

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert '⚠️ IAM analysis failed: IAM analysis failed' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_disk_usage_exception(mock_aws_clients):
    """Test analyze_canary_failures when disk usage analysis fails."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'ENOSPC: no space left on device'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.extract_disk_memory_usage_metrics'
        ) as mock_metrics:
            mock_insights.return_value = 'Telemetry insights'
            # Make disk usage analysis raise an exception
            mock_metrics.side_effect = Exception('Disk usage analysis failed')

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert (
                '⚠️ Could not generate disk usage debugging code: Disk usage analysis failed'
                in result
            )


@pytest.mark.asyncio
async def test_analyze_canary_failures_memory_usage_exception(mock_aws_clients):
    """Test analyze_canary_failures when memory usage analysis fails."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {
                'State': 'FAILED',
                'StateReason': 'Protocol error (Target.activateTarget): Session closed',
            },
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.extract_disk_memory_usage_metrics'
        ) as mock_metrics:
            mock_insights.return_value = 'Telemetry insights'
            # Make memory usage analysis raise an exception
            mock_metrics.side_effect = Exception('Memory usage analysis failed')

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert (
                '⚠️ Could not collect memory usage metrics: Memory usage analysis failed' in result
            )


@pytest.mark.asyncio
async def test_analyze_canary_failures_har_timeout_exception(mock_aws_clients):
    """Test analyze_canary_failures when HAR timeout analysis fails."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timed out after 30000ms'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {
        'Name': 'test-canary',
        'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
    }

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    # Mock S3 to return HAR files
    mock_aws_clients['s3_client'].list_objects_v2.return_value = {
        'Contents': [
            {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
        ]
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_har_file'
        ) as mock_har:
            mock_insights.return_value = 'Telemetry insights'
            # Make HAR analysis raise an exception
            mock_har.side_effect = Exception('HAR analysis failed')

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert '⚠️ HAR analysis failed: HAR analysis failed' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_success_artifacts_exception(mock_aws_clients):
    """Test analyze_canary_failures when success artifacts retrieval fails."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        },
        {
            'Id': 'success-run',
            'Status': {'State': 'PASSED'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        },
    ]

    mock_canary = {
        'Name': 'test-canary',
        'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
    }

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    # Mock S3 to return failure artifacts but fail on success artifacts
    def s3_side_effect(*args, **kwargs):
        prefix = kwargs.get('Prefix', '')
        if 'success' in prefix or len(prefix.split('/')) > 5:  # Simulate success path failure
            raise Exception('Success artifacts access failed')
        return {
            'Contents': [
                {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
            ]
        }

    mock_aws_clients['s3_client'].list_objects_v2.side_effect = s3_side_effect

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_har_file'
        ) as mock_har:
            mock_insights.return_value = 'Telemetry insights'
            mock_har.return_value = {
                'failed_requests': 2,
                'total_requests': 10,
                'request_details': [],
            }

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            # Should still process failure artifacts even if success artifacts fail
            assert '🔍 Comprehensive Failure Analysis for test-canary' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_no_failure_timestamp(mock_aws_clients):
    """Test analyze_canary_failures when failure has no timestamp."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
            'Timeline': {},  # No Started timestamp
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        mock_insights.return_value = 'Telemetry insights'

        result = await analyze_canary_failures('test-canary', 'us-east-1')

        assert '📋 No failure timestamp available for targeted log analysis' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_log_analysis_failure(mock_aws_clients):
    """Test analyze_canary_failures when log analysis fails."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_canary_logs_with_time_window'
        ) as mock_logs:
            mock_insights.return_value = 'Telemetry insights'
            mock_logs.return_value = {
                'status': 'failed',
                'insights': ['Log analysis failed due to missing log group'],
            }

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            assert '📋 Log analysis failed due to missing log group' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_main_exception(mock_aws_clients):
    """Test analyze_canary_failures when main function fails."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    # Make get_canary_runs raise an exception
    mock_aws_clients['synthetics_client'].get_canary_runs.side_effect = Exception('API error')

    result = await analyze_canary_failures('test-canary', 'us-east-1')

    assert '❌ Error in comprehensive failure analysis: API error' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_no_har_files_navigation_timeout(mock_aws_clients):
    """Test analyze_canary_failures navigation timeout without HAR files."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timed out after 30000ms'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        mock_insights.return_value = 'Telemetry insights'

        result = await analyze_canary_failures('test-canary', 'us-east-1')

        assert 'NAVIGATION TIMEOUT DETECTED:' in result
        assert 'No HAR files available for detailed analysis' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_artifact_location_without_s3_prefix(mock_aws_clients):
    """Test analyze_canary_failures with artifact location without s3:// prefix."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {
        'Name': 'test-canary',
        'ArtifactS3Location': 'test-bucket/canary/us-east-1/test-canary',  # No s3:// prefix
    }

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    # Mock S3 to return artifacts
    mock_aws_clients['s3_client'].list_objects_v2.return_value = {
        'Contents': [
            {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
        ]
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_har_file'
        ) as mock_har:
            mock_insights.return_value = 'Telemetry insights'
            mock_har.return_value = {
                'failed_requests': 1,
                'total_requests': 5,
                'request_details': [],
            }

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            # Should still process artifacts even without s3:// prefix
            assert 'FAILURE ANALYSIS' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_empty_base_path(mock_aws_clients):
    """Test analyze_canary_failures with empty base path."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        }
    ]

    mock_canary = {
        'Name': 'test-canary',
        'ArtifactS3Location': 's3://test-bucket',  # Only bucket, no path
    }

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    # Mock S3 to return artifacts
    mock_aws_clients['s3_client'].list_objects_v2.return_value = {
        'Contents': [
            {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
        ]
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_har_file'
        ) as mock_har:
            mock_insights.return_value = 'Telemetry insights'
            mock_har.return_value = {
                'failed_requests': 1,
                'total_requests': 5,
                'request_details': [],
            }

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            # Should construct default canary path when base_path is empty
            assert 'FAILURE ANALYSIS' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_multiple_failure_causes(mock_aws_clients):
    """Test analyze_canary_failures with multiple different failure causes."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run-1',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        },
        {
            'Id': 'failed-run-2',
            'Status': {'State': 'FAILED', 'StateReason': 'Access denied'},
            'Timeline': {'Started': '2024-01-01T00:01:00Z'},
        },
        {
            'Id': 'success-run',
            'Status': {'State': 'PASSED'},
            'Timeline': {'Started': '2023-12-31T23:59:00Z'},
        },
    ]

    mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        mock_insights.return_value = 'Telemetry insights'

        result = await analyze_canary_failures('test-canary', 'us-east-1')

        assert 'Multiple failure causes (2 different issues):' in result
        assert '1. **Navigation timeout** (1 occurrences)' in result
        assert '2. **Access denied** (1 occurrences)' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_no_failure_time_fallback(mock_aws_clients):
    """Test analyze_canary_failures fallback when no failure time."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run',
            'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
            'Timeline': {},  # No Started time
        }
    ]

    mock_canary = {
        'Name': 'test-canary',
        'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
    }

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}

    # Mock S3 to return artifacts
    mock_aws_clients['s3_client'].list_objects_v2.return_value = {
        'Contents': [
            {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
        ]
    }

    with patch(
        'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
    ) as mock_insights:
        with patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_har_file'
        ) as mock_har:
            mock_insights.return_value = 'Telemetry insights'
            mock_har.return_value = {
                'failed_requests': 1,
                'total_requests': 5,
                'request_details': [],
            }

            result = await analyze_canary_failures('test-canary', 'us-east-1')

            # Should use current time when no failure time available
            assert 'FAILURE ANALYSIS' in result


def test_filter_operation_targets_fault_to_availability():
    """Test _filter_operation_targets converts Fault to Availability."""
    provided = [
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'test-service'},
                    'Operation': 'GET /api',
                    'MetricType': 'Fault',
                }
            },
        }
    ]

    operation_targets, has_wildcards = _filter_operation_targets(provided)

    # Verify the MetricType was changed from Fault to Availability
    assert len(operation_targets) == 1
    assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Availability'
    assert has_wildcards is False


def test_filter_operation_targets_non_fault_unchanged():
    """Test _filter_operation_targets leaves non-Fault MetricTypes unchanged."""
    provided = [
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'test-service'},
                    'Operation': 'GET /api',
                    'MetricType': 'Latency',
                }
            },
        },
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'test-service-2'},
                    'Operation': 'POST /api',
                    'MetricType': 'Error',
                }
            },
        },
    ]

    operation_targets, has_wildcards = _filter_operation_targets(provided)

    # Verify non-Fault MetricTypes are unchanged
    assert len(operation_targets) == 2
    assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Latency'
    assert operation_targets[1]['Data']['ServiceOperation']['MetricType'] == 'Error'
    assert has_wildcards is False


def test_filter_operation_targets_multiple_fault_conversions():
    """Test _filter_operation_targets converts multiple Fault entries to Availability."""
    provided = [
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'service-1'},
                    'Operation': 'GET /api',
                    'MetricType': 'Fault',
                }
            },
        },
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'service-2'},
                    'Operation': 'POST /api',
                    'MetricType': 'Latency',
                }
            },
        },
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'service-3'},
                    'Operation': 'PUT /api',
                    'MetricType': 'Fault',
                }
            },
        },
    ]

    operation_targets, has_wildcards = _filter_operation_targets(provided)

    # Verify multiple Fault entries are converted
    assert len(operation_targets) == 3
    assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Availability'
    assert operation_targets[1]['Data']['ServiceOperation']['MetricType'] == 'Latency'
    assert operation_targets[2]['Data']['ServiceOperation']['MetricType'] == 'Availability'
    assert has_wildcards is False


def test_filter_operation_targets_with_wildcards():
    """Test _filter_operation_targets detects wildcards and converts Fault to Availability."""
    provided = [
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': '*payment*'},
                    'Operation': '*GET*',
                    'MetricType': 'Fault',
                }
            },
        }
    ]

    operation_targets, has_wildcards = _filter_operation_targets(provided)

    # Verify wildcard detection and Fault conversion
    assert len(operation_targets) == 1
    assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Availability'
    assert has_wildcards is True


def test_filter_operation_targets_ignores_non_service_operation():
    """Test _filter_operation_targets ignores non-service_operation targets."""
    provided = [
        {
            'Type': 'service',
            'Data': {'Service': {'Type': 'Service', 'Name': 'test-service'}},
        },
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'test-service'},
                    'Operation': 'GET /api',
                    'MetricType': 'Fault',
                }
            },
        },
    ]

    operation_targets, has_wildcards = _filter_operation_targets(provided)

    # Verify only service_operation targets are included
    assert len(operation_targets) == 1
    assert operation_targets[0]['Type'] == 'service_operation'
    assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Availability'
    assert has_wildcards is False


def test_filter_operation_targets_empty_metric_type():
    """Test _filter_operation_targets handles empty MetricType gracefully."""
    provided = [
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'test-service'},
                    'Operation': 'GET /api',
                    'MetricType': '',
                }
            },
        }
    ]

    operation_targets, has_wildcards = _filter_operation_targets(provided)

    # Verify empty MetricType is unchanged
    assert len(operation_targets) == 1
    assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == ''
    assert has_wildcards is False


def test_filter_operation_targets_missing_metric_type():
    """Test _filter_operation_targets handles missing MetricType gracefully."""
    provided = [
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'test-service'},
                    'Operation': 'GET /api',
                    # MetricType is missing
                }
            },
        }
    ]

    operation_targets, has_wildcards = _filter_operation_targets(provided)

    # Verify missing MetricType doesn't cause errors
    assert len(operation_targets) == 1
    # MetricType should remain missing (empty string from .get())
    assert operation_targets[0]['Data']['ServiceOperation'].get('MetricType', '') == ''
    assert has_wildcards is False


def test_filter_operation_targets_case_sensitive():
    """Test _filter_operation_targets is case-sensitive for Fault conversion."""
    provided = [
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'test-service'},
                    'Operation': 'GET /api',
                    'MetricType': 'fault',  # lowercase
                }
            },
        },
        {
            'Type': 'service_operation',
            'Data': {
                'ServiceOperation': {
                    'Service': {'Type': 'Service', 'Name': 'test-service-2'},
                    'Operation': 'POST /api',
                    'MetricType': 'FAULT',  # uppercase
                }
            },
        },
    ]

    operation_targets, has_wildcards = _filter_operation_targets(provided)

    # Verify only exact case "Fault" is converted
    assert len(operation_targets) == 2
    assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'fault'  # unchanged
    assert operation_targets[1]['Data']['ServiceOperation']['MetricType'] == 'FAULT'  # unchanged
    assert has_wildcards is False


@pytest.mark.asyncio
async def test_list_canaries_success(mock_aws_clients):
    """Test list_canaries returns formatted canary list."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    mock_aws_clients['synthetics_client'].describe_canaries.return_value = {
        'Canaries': [
            {
                'Name': 'my-canary',
                'Status': {'State': 'RUNNING', 'StateReason': ''},
                'Schedule': {'Expression': 'rate(5 minutes)'},
                'RuntimeVersion': 'syn-nodejs-puppeteer-13.0',
            },
            {
                'Name': 'stopped-canary',
                'Status': {'State': 'STOPPED', 'StateReason': ''},
                'Schedule': {'Expression': 'rate(1 hour)'},
                'RuntimeVersion': 'syn-nodejs-playwright-5.0',
            },
        ],
        'NextToken': None,
    }

    result = await list_canaries('us-east-1')

    assert 'Found 2 canaries' in result
    assert 'my-canary' in result
    assert 'stopped-canary' in result
    assert 'RUNNING' in result
    assert 'STOPPED' in result
    assert 'syn-nodejs-puppeteer-13.0' in result
    assert 'syn-nodejs-playwright-5.0' in result


@pytest.mark.asyncio
async def test_list_canaries_empty(mock_aws_clients):
    """Test list_canaries when no canaries exist."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    mock_aws_clients['synthetics_client'].describe_canaries.return_value = {
        'Canaries': [],
    }

    result = await list_canaries('us-east-1')
    assert 'No canaries found' in result


@pytest.mark.asyncio
async def test_list_canaries_api_error(mock_aws_clients):
    """Test list_canaries handles API errors."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    mock_aws_clients['synthetics_client'].describe_canaries.side_effect = ClientError(
        {'Error': {'Code': 'AccessDeniedException', 'Message': 'Access denied'}},
        'DescribeCanaries',
    )

    result = await list_canaries('us-east-1')
    assert 'Error listing canaries' in result


@pytest.mark.asyncio
async def test_list_canaries_pagination(mock_aws_clients):
    """Test list_canaries handles pagination."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    mock_aws_clients['synthetics_client'].describe_canaries.side_effect = [
        {
            'Canaries': [
                {
                    'Name': 'canary-1',
                    'Status': {'State': 'RUNNING'},
                    'Schedule': {'Expression': 'rate(5 minutes)'},
                    'RuntimeVersion': 'syn-nodejs-puppeteer-13.0',
                }
            ],
            'NextToken': 'token123',
        },
        {
            'Canaries': [
                {
                    'Name': 'canary-2',
                    'Status': {'State': 'RUNNING'},
                    'Schedule': {'Expression': 'rate(5 minutes)'},
                    'RuntimeVersion': 'syn-nodejs-puppeteer-13.0',
                }
            ],
        },
    ]

    result = await list_canaries('us-east-1')
    assert 'Found 2 canaries' in result
    assert 'canary-1' in result
    assert 'canary-2' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_kb_recommendations_on_failure(mock_aws_clients):
    """Test analyze_canary_failures produces KB recommendations for matching errors."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {
        'CanaryRuns': [
            {
                'Id': 'run-failed-1',
                'Status': {
                    'State': 'FAILED',
                    'StateReason': 'page.goto: Timeout 60000ms exceeded. waiting until "load"',
                },
                'Timeline': {'Started': datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)},
            }
        ]
    }
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {
            'Name': 'test-canary',
            'RuntimeVersion': 'syn-nodejs-playwright-2.0',
            'ArtifactS3Location': '',
        }
    }
    mock_aws_clients['s3_client'].list_objects_v2.return_value = {'Contents': []}

    with (
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
        ) as mock_insights,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_canary_logs_with_time_window'
        ) as mock_cw_logs,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_code'
        ) as mock_code,
    ):
        mock_insights.return_value = ''
        mock_cw_logs.return_value = {
            'status': 'success',
            'time_window': '11:55-12:05',
            'total_events': 3,
            'error_events': [
                {
                    'timestamp': datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
                    'message': 'page.goto: Timeout 60000ms exceeded. waiting until "load"',
                }
            ],
            'insights': [],
        }
        mock_code.return_value = {'error': 'no code'}

        result = await analyze_canary_failures('test-canary', 'us-east-1')

        assert '🔍 Comprehensive Failure Analysis for test-canary' in result
        # The KB should match RUNTIME-001 based on the error message
        assert 'Knowledge Base Recommendations' in result or 'Timeout 60000ms exceeded' in result


@pytest.mark.asyncio
async def test_list_canaries_max_results_truncation(mock_aws_clients):
    """Test list_canaries truncates output when more canaries exist than max_results."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    # Return 20 canaries on first page with a next token (more exist)
    canaries_page1 = [
        {
            'Name': f'canary-{i}',
            'Status': {'State': 'RUNNING'},
            'Schedule': {'Expression': 'rate(5 minutes)'},
            'RuntimeVersion': 'syn-nodejs-puppeteer-13.0',
        }
        for i in range(20)
    ]
    mock_aws_clients['synthetics_client'].describe_canaries.return_value = {
        'Canaries': canaries_page1,
        'NextToken': 'more-exist',
    }

    # max_results=5 should stop after first page and only display 5
    result = await list_canaries('us-east-1', max_results=5)

    assert 'showing first 5' in result
    assert 'canary-0' in result
    assert 'canary-4' in result


@pytest.mark.asyncio
async def test_list_canaries_max_results_clamped(mock_aws_clients):
    """Test list_canaries clamps max_results to valid range."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    mock_aws_clients['synthetics_client'].describe_canaries.return_value = {
        'Canaries': [
            {
                'Name': 'canary-1',
                'Status': {'State': 'RUNNING'},
                'Schedule': {'Expression': 'rate(5 minutes)'},
                'RuntimeVersion': 'syn-nodejs-puppeteer-13.0',
            }
        ],
    }

    # max_results=0 should be clamped to 1
    result = await list_canaries('us-east-1', max_results=0)
    assert 'canary-1' in result

    # max_results=999 should be clamped to 200
    result = await list_canaries('us-east-1', max_results=999)
    assert 'canary-1' in result


@pytest.mark.asyncio
async def test_list_canaries_error_and_stopped_states(mock_aws_clients):
    """Test list_canaries displays correct emojis for different states."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    mock_aws_clients['synthetics_client'].describe_canaries.return_value = {
        'Canaries': [
            {
                'Name': 'error-canary',
                'Status': {'State': 'ERROR', 'StateReason': 'Lambda error'},
                'Schedule': {'Expression': 'rate(5 minutes)'},
                'RuntimeVersion': 'syn-nodejs-puppeteer-13.0',
            },
            {
                'Name': 'unknown-canary',
                'Status': {'State': 'CREATING'},
                'Schedule': {'Expression': 'rate(1 hour)'},
                'RuntimeVersion': 'syn-nodejs-puppeteer-13.0',
            },
        ],
    }

    result = await list_canaries('us-east-1')

    assert '🟠 error-canary' in result
    assert 'Lambda error' in result
    assert '⚪ unknown-canary' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_healthy_with_description(mock_aws_clients):
    """Test analyze_canary_failures runs KB lookup on healthy canary when description is provided."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    # All runs passed — no failures
    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {
        'CanaryRuns': [
            {
                'Id': 'run-1',
                'Status': {'State': 'PASSED', 'StateReason': ''},
                'Timeline': {'Started': datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)},
            }
        ]
    }
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {
            'Name': 'healthy-canary',
            'RuntimeVersion': 'syn-nodejs-puppeteer-10.0',
            'ArtifactS3Location': '',
        }
    }

    result = await analyze_canary_failures(
        'healthy-canary', 'us-east-1', description='visual monitoring baseline keeps resetting'
    )

    assert 'recovering or healthy' in result
    # KB should match RUNTIME-005 based on the description
    assert 'Knowledge Base Recommendations' in result or 'baseline' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_healthy_no_description(mock_aws_clients):
    """Test analyze_canary_failures skips KB on healthy canary without description."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {
        'CanaryRuns': [
            {
                'Id': 'run-1',
                'Status': {'State': 'PASSED', 'StateReason': ''},
                'Timeline': {'Started': datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)},
            }
        ]
    }
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {
            'Name': 'healthy-canary',
            'RuntimeVersion': 'syn-nodejs-puppeteer-10.0',
            'ArtifactS3Location': '',
        }
    }

    result = await analyze_canary_failures('healthy-canary', 'us-east-1')

    assert 'recovering or healthy' in result
    assert 'Knowledge Base Recommendations' not in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_kb_with_cw_logs_enrichment(mock_aws_clients):
    """Test analyze_canary_failures enriches KB context from CW logs when S3 artifacts are available."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {
        'CanaryRuns': [
            {
                'Id': 'run-failed-1',
                'Status': {
                    'State': 'FAILED',
                    'StateReason': 'Canary failed',
                },
                'Timeline': {'Started': datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)},
            }
        ]
    }
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {
            'Name': 'test-canary',
            'RuntimeVersion': 'syn-nodejs-puppeteer-11.0',
            'ArtifactS3Location': 'cw-syn-results-123/canary/test-canary',
        }
    }
    # S3 artifacts available
    mock_aws_clients['s3_client'].list_objects_v2.return_value = {
        'Contents': [
            {'Key': 'canary/test-canary/run-failed-1/logs/log.txt', 'Size': 100},
        ]
    }

    with (
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
        ) as mock_insights,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_canary_logs_with_time_window'
        ) as mock_cw_logs,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_code'
        ) as mock_code,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_log_files'
        ) as mock_log_files,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_screenshots'
        ) as mock_screenshots,
    ):
        mock_insights.return_value = ''
        mock_cw_logs.return_value = {
            'status': 'success',
            'time_window': '11:55-12:05',
            'total_events': 2,
            'error_events': [
                {
                    'timestamp': datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
                    'message': 'panic: runtime error: goroutine stack overflow',
                }
            ],
            'insights': ['Golang panic detected'],
        }
        mock_log_files.return_value = {
            'insights': ['panic detected in runtime'],
            'error_events': [{'message': 'fatal error: goroutine'}],
        }
        mock_screenshots.return_value = {'screenshots': [], 'insights': []}
        mock_code.return_value = {'error': 'no code'}

        result = await analyze_canary_failures('test-canary', 'us-east-1')

        assert '🔍 Comprehensive Failure Analysis for test-canary' in result


@pytest.mark.asyncio
async def test_list_canaries_unknown_state(mock_aws_clients):
    """Test list_canaries displays ⚪ for unknown canary states."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    mock_aws_clients['synthetics_client'].describe_canaries.return_value = {
        'Canaries': [
            {
                'Name': 'weird-canary',
                'Status': {'State': 'CREATING', 'StateReason': ''},
                'Schedule': {'Expression': 'rate(5 minutes)'},
                'RuntimeVersion': 'syn-nodejs-puppeteer-10.0',
                'Timeline': {'LastStarted': 'Never'},
            }
        ]
    }

    result = await list_canaries('us-east-1')
    assert '⚪ weird-canary' in result


@pytest.mark.asyncio
async def test_list_canaries_generic_exception(mock_aws_clients):
    """Test list_canaries handles non-ClientError exceptions."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import list_canaries

    mock_aws_clients['synthetics_client'].describe_canaries.side_effect = RuntimeError(
        'Unexpected failure'
    )

    result = await list_canaries('us-east-1')
    assert 'Error listing canaries' in result
    assert 'Unexpected failure' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_tags_as_list(mock_aws_clients):
    """Test analyze_canary_failures handles canary tags as list of dicts."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run-1',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
            'Timeline': {'Started': '2024-01-01T01:00:00Z'},
        },
        {
            'Id': 'success-run',
            'Status': {'State': 'PASSED'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        },
    ]
    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {
            'Name': 'test-canary',
            'RuntimeVersion': 'syn-nodejs-puppeteer-10.0',
            'ArtifactS3Location': '',
            'Tags': [
                {'Key': 'env', 'Value': 'production'},
                {'Key': 'team', 'Value': 'platform'},
            ],
        }
    }

    with (
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
        ) as mock_insights,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_code'
        ) as mock_code,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_canary_logs_with_time_window'
        ) as mock_cw_logs,
    ):
        mock_insights.return_value = ''
        mock_code.return_value = {'error': 'no code'}
        mock_cw_logs.return_value = {'status': 'no_logs', 'insights': ['No log group found']}

        result = await analyze_canary_failures('test-canary', 'us-east-1')
        assert 'Comprehensive Failure Analysis' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_with_description(mock_aws_clients):
    """Test analyze_canary_failures passes description to KB (line 1577)."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run-1',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
            'Timeline': {'Started': '2024-01-01T01:00:00Z'},
        },
        {
            'Id': 'success-run',
            'Status': {'State': 'PASSED'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        },
    ]
    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {
            'Name': 'test-canary',
            'RuntimeVersion': 'syn-nodejs-puppeteer-10.0',
            'ArtifactS3Location': '',
        }
    }

    with (
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
        ) as mock_insights,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_code'
        ) as mock_code,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_canary_logs_with_time_window'
        ) as mock_cw_logs,
    ):
        mock_insights.return_value = ''
        mock_code.return_value = {'error': 'no code'}
        mock_cw_logs.return_value = {'status': 'no_logs', 'insights': ['No log group found']}

        result = await analyze_canary_failures(
            'test-canary', 'us-east-1', description='canary keeps timing out on login page'
        )
        assert 'Comprehensive Failure Analysis' in result


@pytest.mark.asyncio
async def test_analyze_canary_failures_kb_exception(mock_aws_clients):
    """Test analyze_canary_failures handles KB recommendation exception."""
    from awslabs.cloudwatch_applicationsignals_mcp_server.server import analyze_canary_failures

    mock_runs = [
        {
            'Id': 'failed-run-1',
            'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
            'Timeline': {'Started': '2024-01-01T01:00:00Z'},
        },
        {
            'Id': 'success-run',
            'Status': {'State': 'PASSED'},
            'Timeline': {'Started': '2024-01-01T00:00:00Z'},
        },
    ]
    mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
    mock_aws_clients['synthetics_client'].get_canary.return_value = {
        'Canary': {
            'Name': 'test-canary',
            'RuntimeVersion': 'syn-nodejs-puppeteer-10.0',
            'ArtifactS3Location': '',
        }
    }

    with (
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_metrics_and_service_insights'
        ) as mock_insights,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.get_canary_code'
        ) as mock_code,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.analyze_canary_logs_with_time_window'
        ) as mock_cw_logs,
        patch(
            'awslabs.cloudwatch_applicationsignals_mcp_server.server.CanaryKnowledgeBaseLoader'
        ) as mock_kb_loader,
    ):
        mock_insights.return_value = ''
        mock_code.return_value = {'error': 'no code'}
        mock_cw_logs.return_value = {'status': 'no_logs', 'insights': ['No log group found']}
        mock_kb_loader.get_instance.side_effect = Exception('KB load failed')

        result = await analyze_canary_failures('test-canary', 'us-east-1')
        # Should still return a result despite KB failure
        assert 'Comprehensive Failure Analysis' in result
