"""
Tests for S3 bucket cleanup functionality.

These tests verify that the S3 cleanup utilities correctly identify and delete
IIC-managed deployment buckets while leaving other buckets untouched.
"""

import pytest
import boto3
from moto import mock_aws
from unittest.mock import patch, MagicMock
import click.testing
from click.testing import CliRunner

from iicy.utils.s3_cleanup import (
    empty_and_delete_bucket,
    get_deployment_bucket_names,
    cleanup_deployment_buckets,
    _is_deployment_bucket,
    _empty_bucket,
    IICYBucketCleanupError
)


class TestS3Cleanup:
    """Test cases for S3 cleanup utilities."""
    
    @mock_aws
    def test_get_deployment_bucket_names(self):
        """Test that deployment bucket names are generated correctly."""
        stack_names = ["MyStack", "Another-Stack", "test_stack"]
        expected_buckets = [
            "mystack-deployment-bucket",
            "another-stack-deployment-bucket", 
            "test_stack-deployment-bucket"
        ]
        
        result = get_deployment_bucket_names(stack_names)
        assert result == expected_buckets
    
    @mock_aws
    def test_is_deployment_bucket_with_lambda_objects(self):
        """Test identification of deployment buckets with lambda objects."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "test-deployment-bucket"
        
        # Create bucket and add lambda object
        s3_client.create_bucket(Bucket=bucket_name)
        s3_client.put_object(
            Bucket=bucket_name,
            Key="lambda/test-function.zip",
            Body=b"fake zip content"
        )
        
        assert _is_deployment_bucket(s3_client, bucket_name) is True
    
    @mock_aws
    def test_is_deployment_bucket_empty_with_correct_name(self):
        """Test identification of empty deployment buckets with correct naming."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "test-deployment-bucket"
        
        # Create empty bucket with deployment naming pattern
        s3_client.create_bucket(Bucket=bucket_name)
        
        assert _is_deployment_bucket(s3_client, bucket_name) is True
    
    @mock_aws 
    def test_is_deployment_bucket_wrong_name(self):
        """Test that buckets with wrong names are not identified as deployment buckets."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "some-other-bucket"
        
        # Create bucket with wrong name
        s3_client.create_bucket(Bucket=bucket_name)
        
        assert _is_deployment_bucket(s3_client, bucket_name) is False
    
    @mock_aws
    def test_is_deployment_bucket_with_non_lambda_objects(self):
        """Test that buckets with non-lambda objects are not identified as deployment buckets."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "some-deployment-bucket"
        
        # Create bucket with non-lambda objects
        s3_client.create_bucket(Bucket=bucket_name)
        s3_client.put_object(
            Bucket=bucket_name,
            Key="other/file.txt", 
            Body=b"some content"
        )
        
        assert _is_deployment_bucket(s3_client, bucket_name) is False
    
    @mock_aws
    def test_empty_bucket_with_regular_objects(self):
        """Test emptying bucket with regular objects."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "test-deployment-bucket"
        
        # Create bucket with objects
        s3_client.create_bucket(Bucket=bucket_name)
        s3_client.put_object(Bucket=bucket_name, Key="lambda/func1.zip", Body=b"content1")
        s3_client.put_object(Bucket=bucket_name, Key="lambda/func2.zip", Body=b"content2")
        
        # Empty the bucket
        with patch('click.echo'):  # Suppress output during test
            _empty_bucket(s3_client, bucket_name)
        
        # Verify bucket is empty
        response = s3_client.list_objects_v2(Bucket=bucket_name)
        assert 'Contents' not in response
    
    @mock_aws
    def test_empty_bucket_with_versioned_objects(self):
        """Test emptying bucket with versioned objects and delete markers."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "test-deployment-bucket"
        
        # Create versioned bucket
        s3_client.create_bucket(Bucket=bucket_name)
        s3_client.put_bucket_versioning(
            Bucket=bucket_name,
            VersioningConfiguration={'Status': 'Enabled'}
        )
        
        # Add multiple versions of same object
        s3_client.put_object(Bucket=bucket_name, Key="lambda/func.zip", Body=b"v1")
        s3_client.put_object(Bucket=bucket_name, Key="lambda/func.zip", Body=b"v2") 
        s3_client.delete_object(Bucket=bucket_name, Key="lambda/func.zip")  # Creates delete marker
        
        # Empty the bucket
        with patch('click.echo'):  # Suppress output during test
            _empty_bucket(s3_client, bucket_name)
        
        # Verify all versions and delete markers are gone
        response = s3_client.list_object_versions(Bucket=bucket_name)
        assert 'Versions' not in response
        assert 'DeleteMarkers' not in response
    
    @mock_aws
    def test_empty_and_delete_bucket_success(self):
        """Test successful emptying and deletion of deployment bucket."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "test-deployment-bucket"
        
        # Create bucket with lambda objects
        s3_client.create_bucket(Bucket=bucket_name)
        s3_client.put_object(Bucket=bucket_name, Key="lambda/func.zip", Body=b"content")
        
        # Should succeed without raising exception
        with patch('click.echo'):  # Suppress output during test
            empty_and_delete_bucket(s3_client, bucket_name)
        
        # Verify bucket is gone
        from botocore.exceptions import ClientError
        with pytest.raises(ClientError) as exc_info:
            s3_client.head_bucket(Bucket=bucket_name)
        assert exc_info.value.response['Error']['Code'] in ['404', 'NoSuchBucket']
    
    @mock_aws
    def test_empty_and_delete_bucket_nonexistent(self):
        """Test handling of non-existent bucket."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "nonexistent-deployment-bucket"
        
        # Should not raise exception for non-existent bucket
        with patch('click.echo'):  # Suppress output during test
            empty_and_delete_bucket(s3_client, bucket_name)
    
    @mock_aws
    def test_empty_and_delete_bucket_wrong_type(self):
        """Test that wrong bucket type is skipped."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        bucket_name = "not-a-deployment-bucket"
        
        # Create bucket that shouldn't be identified as deployment bucket
        s3_client.create_bucket(Bucket=bucket_name) 
        s3_client.put_object(Bucket=bucket_name, Key="other/file.txt", Body=b"content")
        
        with patch('click.echo'):  # Suppress output during test
            empty_and_delete_bucket(s3_client, bucket_name)
        
        # Bucket should still exist (not deleted)
        s3_client.head_bucket(Bucket=bucket_name)  # Should not raise
    
    @mock_aws
    def test_cleanup_deployment_buckets(self):
        """Test cleanup of multiple deployment buckets."""
        s3_client = boto3.client('s3', region_name='us-east-1')
        session = boto3.Session()
        
        stack_names = ["Stack1", "Stack2"]
        bucket_names = ["stack1-deployment-bucket", "stack2-deployment-bucket"]
        
        # Create deployment buckets
        for bucket_name in bucket_names:
            s3_client.create_bucket(Bucket=bucket_name)
            s3_client.put_object(Bucket=bucket_name, Key="lambda/func.zip", Body=b"content")
        
        # Create unrelated bucket that should not be touched
        unrelated_bucket = "some-other-bucket"
        s3_client.create_bucket(Bucket=unrelated_bucket)
        s3_client.put_object(Bucket=unrelated_bucket, Key="data.txt", Body=b"data")
        
        with patch('click.echo'):  # Suppress output during test
            cleanup_deployment_buckets(stack_names, session)
        
        # Verify deployment buckets are gone
        from botocore.exceptions import ClientError
        for bucket_name in bucket_names:
            with pytest.raises(ClientError) as exc_info:
                s3_client.head_bucket(Bucket=bucket_name)
            assert exc_info.value.response['Error']['Code'] in ['404', 'NoSuchBucket']
        
        # Verify unrelated bucket still exists
        s3_client.head_bucket(Bucket=unrelated_bucket)  # Should not raise
    
    @mock_aws 
    def test_cleanup_deployment_buckets_with_failures(self):
        """Test cleanup continues despite individual bucket failures."""
        session = boto3.Session()
        stack_names = ["Stack1", "Stack2"]
        
        with patch('click.echo'):  # Suppress output during test
            # Mock empty_and_delete_bucket to raise error for first bucket
            with patch('iicy.utils.s3_cleanup.empty_and_delete_bucket') as mock_delete:
                def side_effect(s3_client, bucket_name):
                    if bucket_name == "stack1-deployment-bucket":
                        raise IICYBucketCleanupError("Simulated failure")
                    # Second bucket succeeds (no exception)
                
                mock_delete.side_effect = side_effect
                
                # Should complete without raising exception
                cleanup_deployment_buckets(stack_names, session)
                
                # Should have been called for both buckets
                assert mock_delete.call_count == 2


class TestCLIIntegration:
    """Test CLI integration for S3 cleanup."""
    
    def test_destroy_command_calls_s3_cleanup(self):
        """Test that destroy command calls S3 cleanup."""
        from iicy.cli import destroy
        
        # Mock all the dependencies
            with patch('iicy.cli.InfraPipeline') as mock_pipeline:
                with patch('iicy.cli.get_aws_client') as mock_get_client:
                    with patch('iicy.utils.s3_cleanup.cleanup_deployment_buckets') as mock_cleanup:
                    with patch('boto3.Session') as mock_session:
                        with patch('os.path.exists', return_value=False):  # No infra.yml
                            
                            # Setup mocks
                            mock_pipeline.return_value.get_stack_names.return_value = ['TestStack']
                            mock_cf_client = MagicMock()
                            mock_get_client.return_value = mock_cf_client
                            
                            # Mock successful stack deletion
                            mock_cf_client.delete_stack.return_value = {}
                            mock_waiter = MagicMock()
                            mock_cf_client.get_waiter.return_value = mock_waiter
                            
                            runner = CliRunner()
                            
                            # Run destroy command
                            result = runner.invoke(destroy, [
                                '--path', '.'
                                # delete-infra-file is True by default, so no flag needed
                            ])
                            
                            # Verify S3 cleanup was called
                            mock_cleanup.assert_called_once()
                            call_args = mock_cleanup.call_args
                            assert call_args[0][0] == ['TestStack']  # First arg: stack names
                            
                            # Command should succeed
                            assert result.exit_code == 0
    
    def test_destroy_command_handles_s3_cleanup_failure(self):
        """Test that destroy command continues if S3 cleanup fails."""
        from iicy.cli import destroy
        
        # Mock all the dependencies
            with patch('iicy.cli.InfraPipeline') as mock_pipeline:
                with patch('iicy.cli.get_aws_client') as mock_get_client:
                    with patch('iicy.utils.s3_cleanup.cleanup_deployment_buckets') as mock_cleanup:
                    with patch('boto3.Session') as mock_session:
                        with patch('os.path.exists', return_value=False):  # No infra.yml
                            
                            # Setup mocks
                            mock_pipeline.return_value.get_stack_names.return_value = ['TestStack']
                            mock_cf_client = MagicMock()
                            mock_get_client.return_value = mock_cf_client
                            
                            # Mock successful stack deletion
                            mock_cf_client.delete_stack.return_value = {}
                            mock_waiter = MagicMock()
                            mock_cf_client.get_waiter.return_value = mock_waiter
                            
                            # Mock S3 cleanup failure
                            mock_cleanup.side_effect = Exception("S3 cleanup failed")
                            
                            runner = CliRunner()
                            
                            # Run destroy command
                            result = runner.invoke(destroy, [
                                '--path', '.'
                                # delete-infra-file is True by default, so no flag needed  
                            ])
                            
                            # Command should still succeed despite S3 cleanup failure
                            assert result.exit_code == 0
                            assert "Error during S3 bucket cleanup" in result.output