"""Tests for the example.py module."""

import os
from unittest.mock import Mock, patch

from example import create_distribution, main, perform_analysis
from pvl_p90_client.client.p90_client import AuthenticationError, ServiceError
from pvl_p90_client.grpcclient import uncertaintyMessages_pb2
from pvl_p90_client.helpers.exception import P90ConnectionError


class TestArgumentHandling:
    """Test the command line argument handling."""

    @patch("example.pvl_login.login")
    @patch("example.P90Client")
    @patch("example.load_weather_data_from_pvw_file", return_value=[])
    @patch("example.perform_analysis")
    def test_main_GIVEN_username_arg_WHEN_called_THEN_passes_username_to_login(self, mock_perform_analysis, mock_load_weather, mock_client_class, mock_login):
        """Test that main function passes provided username to login."""
        mock_login.return_value = Mock()

        main(username="test_user")

        mock_login.assert_called_once_with(username="test_user", password=None)

    @patch("example.pvl_login.login")
    @patch("example.P90Client")
    @patch("example.load_weather_data_from_pvw_file", return_value=[])
    @patch("example.perform_analysis")
    def test_main_GIVEN_password_arg_WHEN_called_THEN_passes_password_to_login(self, mock_perform_analysis, mock_load_weather, mock_client_class, mock_login):
        """Test that main function passes provided password to login."""
        mock_login.return_value = Mock()

        main(password="test_pass")

        mock_login.assert_called_once_with(username=None, password="test_pass")

    @patch("example.pvl_login.login")
    @patch("example.P90Client")
    @patch("example.load_weather_data_from_pvw_file", return_value=[])
    @patch("example.perform_analysis")
    def test_main_GIVEN_both_credentials_WHEN_called_THEN_passes_both_to_login(self, mock_perform_analysis, mock_load_weather, mock_client_class, mock_login):
        """Test that main function passes both username and password to login."""
        mock_login.return_value = Mock()

        main(username="test_user", password="test_pass")

        mock_login.assert_called_once_with(username="test_user", password="test_pass")

    @patch("example.pvl_login.login")
    @patch("example.P90Client")
    @patch("example.load_weather_data_from_pvw_file", return_value=[])
    @patch("example.perform_analysis")
    def test_main_GIVEN_version_arg_WHEN_called_THEN_passes_version_to_client(self, mock_perform_analysis, mock_load_weather, mock_client_class, mock_login):
        """Test that main function passes version to P90Client constructor."""
        mock_login.return_value = Mock()

        main(version="test-version")

        mock_client_class.assert_called_once_with(version="test-version")

    @patch("example.pvl_login.login")
    @patch("example.P90Client")
    @patch("example.load_weather_data_from_pvw_file", return_value=[])
    @patch("example.perform_analysis")
    def test_main_GIVEN_no_version_WHEN_called_THEN_creates_client_without_version(
        self, mock_perform_analysis, mock_load_weather, mock_client_class, mock_login
    ):
        """Test that main function creates client without version when none provided."""
        mock_login.return_value = Mock()

        main()

        mock_client_class.assert_called_once_with()

    @patch("example.pvl_login.login")
    @patch("example.P90Client")
    @patch("example.load_weather_data_from_pvw_file", return_value=[])
    @patch("example.perform_analysis")
    def test_main_GIVEN_all_args_WHEN_called_THEN_uses_all_values(self, mock_perform_analysis, mock_load_weather, mock_client_class, mock_login):
        """Test that main function uses all provided arguments correctly."""
        mock_login.return_value = Mock()

        main(username="user", password="pass", version="v1.0")

        mock_login.assert_called_once_with(username="user", password="pass")
        mock_client_class.assert_called_once_with(version="v1.0")


class TestCreateDistribution:
    """Test the create_distribution function."""

    @patch("example.build_distribution")
    @patch("example.build_gaussian_distribution")
    @patch("example.build_weibull_distribution")
    def test_create_distribution_GIVEN_no_params_WHEN_called_THEN_creates_three_Distributions(self, mock_weibull, mock_gaussian, mock_build_distribution):
        # Setup mocks
        mock_gaussian.return_value = uncertaintyMessages_pb2.DistributionFunction()
        mock_weibull.return_value = uncertaintyMessages_pb2.DistributionFunction()
        mock_build_distribution.return_value = uncertaintyMessages_pb2.Distribution()

        result = create_distribution()

        assert len(result) == 3
        assert mock_build_distribution.call_count == 3

        # Verify the three different distribution inputs were used
        call_args_list = mock_build_distribution.call_args_list
        targets = [call.kwargs["input"] for call in call_args_list]
        expected_targets = [
            uncertaintyMessages_pb2.DistributionInput.GHI,
            uncertaintyMessages_pb2.DistributionInput.SoilingFront,
            uncertaintyMessages_pb2.DistributionInput.Availability,
        ]
        assert targets == expected_targets

    @patch("example.build_distribution")
    def test_create_distribution_GIVEN_valid_setup_WHEN_called_THEN_returns_distribution_list(self, mock_build_distribution):
        mock_distribution = uncertaintyMessages_pb2.Distribution()
        mock_build_distribution.return_value = mock_distribution

        result = create_distribution()

        assert isinstance(result, list)
        assert all(isinstance(mod, uncertaintyMessages_pb2.Distribution) for mod in result)


class TestPerformAnalysis:
    """Test the perform_analysis function."""

    def setup_method(self):  # pylint: disable=invalid-name
        self.mock_client = Mock()  # pylint: disable=attribute-defined-outside-init
        self.mock_request = uncertaintyMessages_pb2.UncertaintyRequest()  # pylint: disable=attribute-defined-outside-init
        self.mock_credentials = Mock()  # pylint: disable=attribute-defined-outside-init

    def test_perform_analysis_GIVEN_successful_response_WHEN_called_THEN_displays_results(self):
        # Create mock summary with results
        summary = uncertaintyMessages_pb2.UncertaintySummary()

        # Add P-values for multiple years
        pvalue1 = summary.YearlyPValue.add()
        pvalue1.Year = 2024
        pvalue1.P = 50
        pvalue1.P50Deviation = 0.95

        pvalue2 = summary.YearlyPValue.add()
        pvalue2.Year = 2025
        pvalue2.P = 95
        pvalue2.P50Deviation = 0.85

        # Add P50 deviations
        p50_dev1 = summary.YearlyP50Deviation.add()
        p50_dev1.Year = 2024
        p50_dev1.Value = 1.0

        p50_dev2 = summary.YearlyP50Deviation.add()
        p50_dev2.Year = 2025
        p50_dev2.Value = 0.98

        # Add histograms for multiple years
        histogram1 = summary.YearlyHistogram.add()
        histogram1.Year = 2024
        histogram1.bins.extend([10, 20, 30, 15])

        histogram2 = summary.YearlyHistogram.add()
        histogram2.Year = 2025
        histogram2.bins.extend([8, 25, 35, 12])

        self.mock_client.send_request.return_value = (summary, None)

        with patch("builtins.print") as mock_print:
            perform_analysis(self.mock_client, self.mock_request, self.mock_credentials)

        # Verify client was called correctly
        self.mock_client.send_request.assert_called_once_with(self.mock_request, self.mock_credentials, timeout=30.0)

        # Verify output messages
        print_calls = [call.args[0] for call in mock_print.call_args_list]

        # Verify main progress and completion messages
        assert "Starting P90 analysis..." in print_calls
        assert any("Analysis completed in" in call for call in print_calls)  # Check timing message exists anywhere

        # Verify P-values section
        assert "\nResults: 2 yearly P-values calculated" in print_calls
        assert "  Year 2024: P50 = 0.950000 of P50" in print_calls
        assert "  Year 2025: P95 = 0.850000 of P50" in print_calls

        # Verify P50 deviations section
        assert "\nYearly P50 Deviations to first year P50 value:" in print_calls
        assert "  Year 2024: 1.000000" in print_calls
        assert "  Year 2025: 0.980000" in print_calls

        # Verify histogram section
        assert "\nHistogram data showing distribution of simulations across uncertainty ranges:" in print_calls
        assert "  Year 2024: 10 20 30 15" in print_calls
        assert "  Year 2025: 8 25 35 12" in print_calls

    def test_perform_analysis_GIVEN_no_summary_WHEN_called_THEN_displays_warning(self):
        self.mock_client.send_request.return_value = (None, None)

        with patch("builtins.print") as mock_print:
            perform_analysis(self.mock_client, self.mock_request, self.mock_credentials)

        print_calls = [call.args[0] for call in mock_print.call_args_list]
        assert "⚠️  No results received from analysis" in print_calls

    def test_perform_analysis_GIVEN_exception_WHEN_called_THEN_exception_is_propagated(self):
        """Test that perform_analysis lets exceptions bubble up to caller."""
        # Test with AuthenticationError
        self.mock_client.send_request.side_effect = AuthenticationError("Auth failed")

        # The function should not catch the exception, so it should be raised
        import pytest

        with pytest.raises(AuthenticationError, match="Auth failed"):
            perform_analysis(self.mock_client, self.mock_request, self.mock_credentials)

        # Test with P90ConnectionError
        self.mock_client.send_request.side_effect = P90ConnectionError("Connection failed")

        with pytest.raises(P90ConnectionError, match="Connection failed"):
            perform_analysis(self.mock_client, self.mock_request, self.mock_credentials)

        # Test with ServiceError
        self.mock_client.send_request.side_effect = ServiceError("Service failed")

        with pytest.raises(ServiceError, match="Service failed"):
            perform_analysis(self.mock_client, self.mock_request, self.mock_credentials)

        # Test with generic RuntimeError
        self.mock_client.send_request.side_effect = RuntimeError("Unexpected error")

        with pytest.raises(RuntimeError, match="Unexpected error"):
            perform_analysis(self.mock_client, self.mock_request, self.mock_credentials)


class TestMain:
    """Test the main function."""

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    @patch("example.load_weather_data_from_pvw_file")
    @patch("builtins.print")
    def test_main_GIVEN_successful_execution_WHEN_called_THEN_completes_workflow(self, mock_print, mock_load_weather, mock_login, mock_client_class):
        # Setup minimal external mocks only
        mock_client = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client
        mock_credentials = Mock()
        mock_login.return_value = mock_credentials

        # Create realistic weather data using actual protobuf structure
        temp_request = uncertaintyMessages_pb2.UncertaintyRequest()
        for i in range(5):  # Just a few data points for testing
            weather_point = temp_request.TMYDataSet.TimeStepDataPoints.add()
            weather_point.Weather.AmbientTemp = 20.0 + i
            weather_point.Weather.GHI = 500.0 + (i * 100)
        weather_data = list(temp_request.TMYDataSet.TimeStepDataPoints)
        mock_load_weather.return_value = weather_data

        # Create realistic analysis result
        summary = uncertaintyMessages_pb2.UncertaintySummary()
        pvalue = summary.YearlyPValue.add()
        pvalue.Year = 2024
        pvalue.P = 50
        pvalue.P50Deviation = 0.95

        histogram = summary.YearlyHistogram.add()
        histogram.bins.extend([10, 20, 30])

        mock_client.send_request.return_value = (summary, None)

        # Run the actual main function - this will use real create_modifier, build_module_info, etc.
        main()

        # Verify external calls were made correctly
        mock_login.assert_called_once()
        mock_load_weather.assert_called_once_with("../data/sydney.pvw")
        mock_client.send_request.assert_called_once()

        # Verify the request structure (this tests the real code path)
        call_args = mock_client.send_request.call_args
        request = call_args[0][0]
        credentials = call_args[0][1]

        # Test that real functions were executed correctly
        assert isinstance(request, uncertaintyMessages_pb2.UncertaintyRequest)
        assert len(request.TMYDataSet.TimeStepDataPoints) == 5
        assert len(request.Distributions) == 3  # Should be created by real create_modifier()
        assert request.SimulationOptions.NumberOfYears == 5  # Default from real build_request
        assert request.SimulationOptions.NumberOfSimulations == 50  # Default from real build_request
        assert credentials == mock_credentials
        assert call_args[1]["timeout"] == 30.0

        # Verify progress messages were printed
        print_calls = [call.args[0] for call in mock_print.call_args_list]
        assert "Connecting to PV Lighthouse..." in print_calls
        assert "Authenticating..." in print_calls
        assert "Authentication successful!" in print_calls
        assert "Loading weather data..." in print_calls
        assert f"Loaded {len(weather_data)} weather data points" in print_calls
        assert "Creating request..." in print_calls
        assert "Built request with 3 distributions" in print_calls

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    @patch("builtins.print")
    def test_main_GIVEN_authentication_error_WHEN_called_THEN_handles_error(self, mock_print, mock_login, mock_client_class):
        mock_client = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client
        mock_login.side_effect = AuthenticationError("Invalid credentials")

        main()

        print_calls = [call.args[0] for call in mock_print.call_args_list]
        assert any("❌ Authentication failed: Invalid credentials" in call for call in print_calls)

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    @patch("example.load_weather_data_from_pvw_file")
    @patch("builtins.print")
    def test_main_GIVEN_weather_data_error_WHEN_called_THEN_handles_error(self, mock_print, mock_load_weather, mock_login, mock_client_class):
        mock_client = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client
        mock_login.return_value = Mock()
        mock_load_weather.return_value = None  # Simulate failed load

        main()

        print_calls = [call.args[0] for call in mock_print.call_args_list]
        assert any("❌ Failed to load weather data:" in call for call in print_calls)

    @patch("example.P90Client")
    @patch("builtins.print")
    def test_main_GIVEN_connection_error_WHEN_called_THEN_handles_error(self, mock_print, mock_client_class):
        mock_client_class.side_effect = P90ConnectionError("Cannot connect")

        main()

        print_calls = [call.args[0] for call in mock_print.call_args_list]
        assert any("❌ Failed to connect to uncertainty service: Cannot connect" in call for call in print_calls)

    @patch("example.P90Client")
    @patch("builtins.print")
    def test_main_GIVEN_keyboard_interrupt_WHEN_called_THEN_handles_error(self, mock_print, mock_client_class):
        mock_client_class.side_effect = KeyboardInterrupt()

        main()

        print_calls = [call.args[0] for call in mock_print.call_args_list]
        assert any("❌ Unexpected error:" in call for call in print_calls)


class TestMainIntegration:
    """Integration tests for the main workflow."""

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    @patch("example.load_weather_data_from_pvw_file")
    def test_main_integration_GIVEN_end_to_end_scenario_WHEN_called_THEN_executes_complete_workflow(self, mock_load_weather, mock_login, mock_client_class):
        # Setup realistic mock data
        mock_client = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client
        mock_credentials = Mock()
        mock_login.return_value = mock_credentials

        # Create realistic weather data
        temp_request = uncertaintyMessages_pb2.UncertaintyRequest()
        for i in range(24):  # 24 hours of data
            weather_point = temp_request.TMYDataSet.TimeStepDataPoints.add()
            weather_point.Weather.AmbientTemp = 20.0 + (i * 0.5)
            weather_point.Weather.GHI = 100.0 + (i * 30.0)
        weather_data = list(temp_request.TMYDataSet.TimeStepDataPoints)
        mock_load_weather.return_value = weather_data

        # Create realistic analysis result
        summary = uncertaintyMessages_pb2.UncertaintySummary()
        for year in [2024, 2025, 2026]:
            for p in [5, 50, 95]:
                pvalue = summary.YearlyPValue.add()
                pvalue.Year = year
                pvalue.P = p
                pvalue.P50Deviation = 0.9 + (p * 0.001)

        mock_client.send_request.return_value = (summary, None)

        with patch("builtins.print"):
            main()

        # Verify the complete workflow executed
        mock_load_weather.assert_called_once_with("../data/sydney.pvw")
        mock_login.assert_called_once()
        mock_client.send_request.assert_called_once()

        # Verify the request was built correctly
        call_args = mock_client.send_request.call_args
        request = call_args[0][0]
        credentials = call_args[0][1]

        assert isinstance(request, uncertaintyMessages_pb2.UncertaintyRequest)
        assert len(request.TMYDataSet.TimeStepDataPoints) == 24
        assert len(request.Distributions) == 3  # GHI, FrontSoiling, Availability
        assert credentials == mock_credentials
        assert call_args[1]["timeout"] == 30.0


class TestMainExitCodes:
    """Test the main function returns correct exit codes for different error conditions with minimal mocking."""

    # Build path relative to project root (works in both local and CI environments)
    _project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    WEATHER_DATA_PATH = os.path.join(_project_root, "data", "sydney.pvw")

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    def test_main_GIVEN_successful_execution_WHEN_called_THEN_returns_exit_code_0(self, mock_login, mock_client_class):
        """Test that main returns exit code 0 on successful execution."""

        # Setup successful authentication
        mock_login.return_value = Mock()

        # Setup successful P90 client with a real summary response
        mock_client_instance = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client_instance

        # Create a real UncertaintySummary with proper structure
        from pvl_p90_client.grpcclient import uncertaintyMessages_pb2

        summary = uncertaintyMessages_pb2.UncertaintySummary()
        pvalue = summary.YearlyPValue.add()
        pvalue.Year = 2024
        pvalue.P = 50
        pvalue.P50Deviation = 0.123456
        mock_client_instance.send_request.return_value = (summary, None)

        with patch("example.WEATHER_DATA_FILE", self.WEATHER_DATA_PATH):
            exit_code = main()

        assert exit_code == 0

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    def test_main_GIVEN_authentication_error_WHEN_called_THEN_returns_exit_code_1(self, mock_login, mock_client_class):
        """Test that main returns exit code 1 on authentication error."""

        # Setup authentication failure
        mock_login.side_effect = AuthenticationError("Authentication failed")

        mock_client_instance = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client_instance

        with patch("example.WEATHER_DATA_FILE", self.WEATHER_DATA_PATH):
            with patch("builtins.print"):  # Suppress error output
                exit_code = main()

        assert exit_code == 1

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    def test_main_GIVEN_weather_data_error_WHEN_called_THEN_returns_exit_code_2(self, mock_login, mock_client_class):
        """Test that main returns exit code 2 on weather data loading error."""
        # Setup successful authentication
        mock_login.return_value = Mock()

        # Setup client but it won't be used since weather data loading will fail
        mock_client_instance = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client_instance

        # Patch with non-existent file to trigger WeatherDataError
        with patch("example.WEATHER_DATA_FILE", "/nonexistent/file.pvw"):
            with patch("builtins.print"):  # Suppress error output
                exit_code = main()

        assert exit_code == 2

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    def test_main_GIVEN_connection_error_WHEN_called_THEN_returns_exit_code_3(self, mock_login, mock_client_class):
        """Test that main returns exit code 3 on connection error."""

        # Setup successful authentication
        mock_login.return_value = Mock()

        # Setup client to raise connection error
        mock_client_instance = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client_instance
        mock_client_instance.send_request.side_effect = P90ConnectionError("Connection failed")

        with patch("example.WEATHER_DATA_FILE", self.WEATHER_DATA_PATH):
            with patch("builtins.print"):  # Suppress error output
                exit_code = main()

        assert exit_code == 3

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    def test_main_GIVEN_service_error_WHEN_called_THEN_returns_exit_code_5(self, mock_login, mock_client_class):
        """Test that main returns exit code 5 on service error."""

        # Setup successful authentication
        mock_login.return_value = Mock()

        # Setup client to raise service error
        mock_client_instance = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client_instance
        mock_client_instance.send_request.side_effect = ServiceError("Service error")

        with patch("example.WEATHER_DATA_FILE", self.WEATHER_DATA_PATH):
            with patch("builtins.print"):  # Suppress error output
                exit_code = main()

        assert exit_code == 5

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    def test_main_GIVEN_runtime_error_WHEN_called_THEN_returns_exit_code_4(self, mock_login, mock_client_class):
        """Test that main returns exit code 4 on unexpected runtime error."""

        # Setup successful authentication
        mock_login.return_value = Mock()

        # Setup client to raise runtime error
        mock_client_instance = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client_instance
        mock_client_instance.send_request.side_effect = RuntimeError("Unexpected error")

        with patch("example.WEATHER_DATA_FILE", self.WEATHER_DATA_PATH):
            with patch("builtins.print"):  # Suppress error output
                exit_code = main()

        assert exit_code == 4

    @patch("example.P90Client")
    @patch("example.pvl_login.login")
    def test_main_GIVEN_keyboard_interrupt_WHEN_called_THEN_returns_exit_code_4(self, mock_login, mock_client_class):
        """Test that main returns exit code 4 on keyboard interrupt."""

        # Setup successful authentication
        mock_login.return_value = Mock()

        # Setup client to raise keyboard interrupt
        mock_client_instance = Mock()
        mock_client_class.return_value.__enter__.return_value = mock_client_instance
        mock_client_instance.send_request.side_effect = KeyboardInterrupt()

        with patch("example.WEATHER_DATA_FILE", self.WEATHER_DATA_PATH):
            with patch("builtins.print"):  # Suppress error output
                exit_code = main()

        assert exit_code == 4
