import pytest
from unittest.mock import mock_open, patch
import yaml
import os
import shutil
from merge_yaml import merge_yaml, process_yaml_files

def test_merge_yaml_basic_merge():
    # Test basic dictionary merge
    base_data = {
        "key1": "value1",
        "key2": "value2"
    }
    overlay_data = {
        "key2": "new_value2",
        "key3": "value3"
    }
    
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(base_data))(),
            mock_open(read_data=yaml.dump(overlay_data))()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        
        assert result["key1"] == "value1"
        assert result["key2"] == "new_value2"
        assert result["key3"] == "value3"

def test_merge_yaml_nested_dict():
    # Test nested dictionary merge
    base_data = {
        "parent": {
            "child1": "value1",
            "child2": "value2"
        }
    }
    overlay_data = {
        "parent": {
            "child2": "new_value2",
            "child3": "value3"
        }
    }
    
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(base_data))(),
            mock_open(read_data=yaml.dump(overlay_data))()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        
        assert result["parent"]["child1"] == "value1"
        assert result["parent"]["child2"] == "new_value2"
        assert result["parent"]["child3"] == "value3"

def test_merge_yaml_list_merge():
    # Test list replacement (not appending)
    base_data = {
        "items": ["item1", "item2"]
    }
    overlay_data = {
        "items": ["item3", "item4"]
    }
    
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(base_data))(),
            mock_open(read_data=yaml.dump(overlay_data))()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        
        # Lists should be replaced, not appended
        assert result["items"] == ["item3", "item4"]

def test_merge_yaml_empty_files():
    # Test with empty YAML files
    base_data = {}
    overlay_data = {}
    
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(base_data))(),
            mock_open(read_data=yaml.dump(overlay_data))()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        assert result == {}

def test_merge_yaml_invalid_yaml():
    # Test with invalid YAML content
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data="invalid: yaml: content")()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        assert result == {}

def test_merge_yaml_file_not_found():
    # Test when files don't exist
    with patch("builtins.open", side_effect=FileNotFoundError):
        result = merge_yaml("nonexistent.yaml", "overlay.yaml")
        assert result == {}

def test_process_yaml_files_success():
    # Test successful file processing
    base_data = {"key": "value"}
    overlay_data = {"key": "new_value"}
    
    base_yaml = yaml.dump(base_data)
    overlay_yaml = yaml.dump(overlay_data)
    
    # Create a dictionary to map file paths to their content
    file_contents = {
        "base.yaml": base_yaml,
        "overlay.yaml": overlay_yaml,
        "output.yaml": ""
    }
    
    def mock_file_open(filename, mode='r'):
        if mode == 'r':
            return mock_open(read_data=file_contents[filename])()
        return mock_open()()
    
    with patch("builtins.open", side_effect=mock_file_open) as m_open, \
         patch("shutil.copy2") as m_copy, \
         patch("yaml.dump") as m_dump:
        
        result = process_yaml_files("base.yaml", "overlay.yaml", "output.yaml")
        assert result is True
        assert m_copy.called
        assert m_dump.called

def test_process_yaml_files_backup_failure():
    # Test when backup creation fails
    with patch("shutil.copy2", side_effect=OSError):
        result = process_yaml_files("base.yaml", "overlay.yaml", "output.yaml")
        assert result is False

def test_process_yaml_files_output_failure():
    # Test when output file writing fails
    base_data = {"key": "value"}
    overlay_data = {"key": "new_value"}
    
    base_yaml = yaml.dump(base_data)
    overlay_yaml = yaml.dump(overlay_data)
    
    mock_file = mock_open()
    with patch("builtins.open", mock_file) as m_open, \
         patch("shutil.copy2"), \
         patch("yaml.dump", side_effect=IOError):
        
        # Configure mock to return different content for different files
        m_open.side_effect = None  # Reset side_effect to use mock_file
        mock_file.read_data = base_yaml  # Set initial read data
        
        result = process_yaml_files("base.yaml", "overlay.yaml", "output.yaml")
        assert result is False

def test_merge_yaml_complex_structure():
    # Test with complex nested structure
    base_data = {
        "servers": {
            "server1": {
                "host": "host1",
                "port": 8080,
                "options": ["opt1", "opt2"]
            }
        },
        "clients": ["client1", "client2"]
    }
    
    overlay_data = {
        "servers": {
            "server1": {
                "port": 9090,
                "options": ["opt3"]
            },
            "server2": {
                "host": "host2",
                "port": 8080
            }
        },
        "clients": ["client3"]
    }
    
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(base_data))(),
            mock_open(read_data=yaml.dump(overlay_data))()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        
        # Check that non-list values are merged properly
        assert result["servers"]["server1"]["host"] == "host1"  # Preserved from base
        assert result["servers"]["server1"]["port"] == 9090    # Updated from overlay
        assert result["servers"]["server2"]["host"] == "host2" # Added from overlay
        
        # Check that lists are replaced, not appended
        assert result["servers"]["server1"]["options"] == ["opt3"]
        assert result["clients"] == ["client3"]

def test_merge_yaml_special_values():
    # Test with special YAML values (null, boolean, numbers)
    base_data = {
        "null_value": None,
        "boolean": True,
        "number": 42
    }
    
    overlay_data = {
        "null_value": "not_null",
        "boolean": False,
        "number": 43
    }
    
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(base_data))(),
            mock_open(read_data=yaml.dump(overlay_data))()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        
        assert result["null_value"] == "not_null"
        assert result["boolean"] is False
        assert result["number"] == 43

def test_merge_yaml_multiple_overlays():
    # Test merging multiple overlays
    base_data = {"key": "base"}
    overlay1_data = {"key": "overlay1"}
    overlay2_data = {"key": "overlay2"}
    
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(base_data))(),
            mock_open(read_data=yaml.dump(overlay1_data))()
        ]
        
        result1 = merge_yaml("base.yaml", "overlay1.yaml")
        assert result1["key"] == "overlay1"
        
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(result1))(),
            mock_open(read_data=yaml.dump(overlay2_data))()
        ]
        
        result2 = merge_yaml("base.yaml", "overlay2.yaml")
        assert result2["key"] == "overlay2"

def test_merge_yaml_prometheus_config():
    # Test merging Prometheus configuration files
    base_config = """
global:
  scrape_interval: 60s
  evaluation_interval: 60s

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - localhost:9093

rule_files:
  - 'recording_rules-generated.yml'
  - 'trigger_rules-generated.yml'

scrape_configs:
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]

otlp:
    promote_resource_attributes: ["host.hostname", "java.version", "kafka.broker.id", "kafka.cluster.id", "kafka.version", "type"]

storage:
  tsdb:
    out_of_order_time_window: 10m
"""

    overlay_config = """
alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - control-center.operator.svc.cluster.local:9093
      scheme: http
"""

    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=base_config)(),
            mock_open(read_data=overlay_config)()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        
        # Verify the merged result
        assert result is not None
        assert isinstance(result, dict)
        
        # Check that global settings are preserved
        assert result["global"]["scrape_interval"] == "60s"
        assert result["global"]["evaluation_interval"] == "60s"
        
        # Check that alerting configuration is completely replaced
        assert len(result["alerting"]["alertmanagers"]) == 1
        assert result["alerting"]["alertmanagers"][0]["static_configs"][0]["targets"][0] == "control-center.operator.svc.cluster.local:9093"
        assert result["alerting"]["alertmanagers"][0]["scheme"] == "http"
        
        # Check that other sections are preserved
        assert len(result["rule_files"]) == 2
        assert result["rule_files"][0] == "recording_rules-generated.yml"
        assert result["rule_files"][1] == "trigger_rules-generated.yml"
        
        # Check that scrape configs are preserved
        assert len(result["scrape_configs"]) == 1
        assert result["scrape_configs"][0]["job_name"] == "prometheus"
        assert result["scrape_configs"][0]["static_configs"][0]["targets"][0] == "localhost:9090"
        
        # Check that otlp and storage settings are preserved
        assert len(result["otlp"]["promote_resource_attributes"]) == 6
        assert result["storage"]["tsdb"]["out_of_order_time_window"] == "10m"

def test_merge_yaml_nested_list_replacement():
    # Test that nested lists are replaced, not appended
    base_data = {
        "parent": {
            "nested_list": ["item1", "item2"],
            "deep": {
                "deeper_list": ["deep1", "deep2"]
            }
        }
    }
    overlay_data = {
        "parent": {
            "nested_list": ["item3"],
            "deep": {
                "deeper_list": ["deep3"]
            }
        }
    }
    
    with patch("builtins.open") as m_open:
        m_open.side_effect = [
            mock_open(read_data=yaml.dump(base_data))(),
            mock_open(read_data=yaml.dump(overlay_data))()
        ]
        
        result = merge_yaml("base.yaml", "overlay.yaml")
        
        assert result["parent"]["nested_list"] == ["item3"]
        assert result["parent"]["deep"]["deeper_list"] == ["deep3"] 