#
# Copyright (c) 2025 Leland Stanford Junior University
# Copyright (c) 2025 The Regents of the University of California
#
# This file is part of pelicun.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# You should have received a copy of the BSD 3-Clause License along with
# pelicun. If not, see <http://www.opensource.org/licenses/>.
#
# Contributors:
# Adam Zsarnóczay

"""These are integration tests on the dlml module of pelicun."""

from __future__ import annotations

import subprocess  # noqa: S404
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, mock_open, patch

import pytest
import requests

from pelicun.tools import dlml


def test_end_to_end_workflow_with_mock() -> None:
    """Test end-to-end workflow with mock GitHub API."""
    # Create a temporary directory that will stand in for DLML_DATA_DIR
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir_path = Path(temp_dir)

        # Mock responses
        mock_release_response = MagicMock()
        mock_release_response.json.return_value = {'target_commitish': '1234567'}

        # Mock API responses and the data directory path
        with patch('requests.get', return_value=mock_release_response), patch(
            'pelicun.tools.dlml._download_file'
        ) as mock_download, patch(
            'pelicun.tools.dlml.DLML_DATA_DIR', new=temp_dir_path
        ), patch(
            'pathlib.Path.open', mock_open(read_data='file1.txt\nfile2.txt')
        ), patch('pelicun.tools.dlml.tqdm'):
            # Download data files
            dlml.download_data_files(version='v1.0.0', use_cache=False)

            # Verify _download_file was called
            assert mock_download.call_count == 3  # model_files.txt + 2 files


def test_with_mock_github_api_responses() -> None:
    """Test with mock GitHub API responses."""
    # Create a temporary directory that will stand in for DLML_DATA_DIR
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir_path = Path(temp_dir)

        # Mock responses for different API calls
        def mock_get_side_effect(url: str, **kwargs: Any) -> MagicMock:  # noqa: ARG001, ANN401
            if 'releases/tags' in url:
                mock_response = MagicMock()
                mock_response.json.return_value = {'target_commitish': '1234567'}
                return mock_response
            if 'commits' in url:
                mock_response = MagicMock()
                mock_response.json.return_value = [{'sha': '1234567'}]
                return mock_response
            mock_response = MagicMock()
            mock_response.iter_content.return_value = [b'chunk1', b'chunk2']
            return mock_response

        # Mock API responses and the data directory path
        with patch('requests.get', side_effect=mock_get_side_effect), patch(
            'pelicun.tools.dlml._download_file'
        ) as mock_download, patch(
            'pelicun.tools.dlml.DLML_DATA_DIR', new=temp_dir_path
        ), patch(
            'pathlib.Path.open', mock_open(read_data='file1.txt\nfile2.txt')
        ), patch('pelicun.tools.dlml.tqdm'):
            # Test with version
            dlml.download_data_files(version='v1.0.0', use_cache=False)
            assert mock_download.call_count == 3  # model_files.txt + 2 files
            mock_download.reset_mock()

            # Test with commit
            dlml.download_data_files(commit='1234567', use_cache=False)
            assert mock_download.call_count == 3  # model_files.txt + 2 files
            mock_download.reset_mock()

            # Test with latest commit
            dlml.download_data_files(commit='latest', use_cache=False)
            assert mock_download.call_count == 3  # model_files.txt + 2 files


# --- Edge Case Tests ---


def test_with_empty_model_files() -> None:
    """Test with empty model_files.txt."""
    # Create a temporary directory that will stand in for DLML_DATA_DIR
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir_path = Path(temp_dir)

        # Mock responses
        mock_release_response = MagicMock()
        mock_release_response.json.return_value = {'target_commitish': '1234567'}

        # Mock API responses and the data directory path
        with patch('requests.get', return_value=mock_release_response), patch(
            'pelicun.tools.dlml._download_file'
        ) as mock_download, patch(
            'pelicun.tools.dlml.DLML_DATA_DIR', new=temp_dir_path
        ), patch('pathlib.Path.open', mock_open(read_data='')), patch(
            'pelicun.tools.dlml.tqdm'
        ):
            # Download data files
            dlml.download_data_files(version='v1.0.0', use_cache=False)

            # Verify _download_file was called only for model_files.txt
            assert mock_download.call_count == 1


def test_with_large_number_of_files() -> None:
    """Test with very large number of files."""
    # Create a temporary directory that will stand in for DLML_DATA_DIR
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir_path = Path(temp_dir)

        # Mock responses
        mock_release_response = MagicMock()
        mock_release_response.json.return_value = {'target_commitish': '1234567'}

        # Create a large list of files
        large_file_list = '\n'.join([f'file{i}.txt' for i in range(1000)])

        # Mock API responses and the data directory path
        with patch('requests.get', return_value=mock_release_response), patch(
            'pelicun.tools.dlml._download_file'
        ) as mock_download, patch(
            'pelicun.tools.dlml.DLML_DATA_DIR', new=temp_dir_path
        ), patch('pathlib.Path.open', mock_open(read_data=large_file_list)), patch(
            'pelicun.tools.dlml.tqdm'
        ):
            # Download data files
            dlml.download_data_files(version='v1.0.0', use_cache=False)

            # Verify _download_file was called for model_files.txt and each file in it
            assert mock_download.call_count == 1001  # model_files.txt + 1000 files


def test_with_special_characters_in_paths() -> None:
    """Test with special characters in file paths."""
    # Create a temporary directory that will stand in for DLML_DATA_DIR
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir_path = Path(temp_dir)

        # Mock responses
        mock_release_response = MagicMock()
        mock_release_response.json.return_value = {'target_commitish': '1234567'}

        # Create a list of files with special characters
        special_file_list = 'path/with spaces/file.txt\npath/with#hash/file.txt\npath/with?question/file.txt'

        # Mock API responses and the data directory path
        with patch('requests.get', return_value=mock_release_response), patch(
            'pelicun.tools.dlml._download_file'
        ) as mock_download, patch(
            'pelicun.tools.dlml.DLML_DATA_DIR', new=temp_dir_path
        ), patch('pathlib.Path.open', mock_open(read_data=special_file_list)), patch(
            'pelicun.tools.dlml.tqdm'
        ):
            # Download data files
            dlml.download_data_files(version='v1.0.0', use_cache=False)

            # Verify _download_file was called for model_files.txt and each file in it
            assert mock_download.call_count == 4  # model_files.txt + 3 files

            # Verify that the URLs contain the expected file paths
            call_args_list = [call[0][0] for call in mock_download.call_args_list]
            assert any('path/with spaces/file.txt' in url for url in call_args_list)
            assert any('path/with#hash/file.txt' in url for url in call_args_list)
            assert any(
                'path/with?question/file.txt' in url for url in call_args_list
            )


def test_with_commented_lines_in_model_files() -> None:
    """Test with commented lines in model_files.txt."""
    # Create a temporary directory that will stand in for DLML_DATA_DIR
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir_path = Path(temp_dir)

        # Mock responses
        mock_release_response = MagicMock()
        mock_release_response.json.return_value = {'target_commitish': '1234567'}

        # Create a list of files with commented lines
        file_list = """# This is a comment
file1.txt
# This is another comment
file2.txt
  # This is a comment with leading whitespace
file3.txt"""

        # Mock API responses and the data directory path
        with patch('requests.get', return_value=mock_release_response), patch(
            'pelicun.tools.dlml._download_file'
        ) as mock_download, patch(
            'pelicun.tools.dlml.DLML_DATA_DIR', new=temp_dir_path
        ), patch('pathlib.Path.open', mock_open(read_data=file_list)), patch(
            'pelicun.tools.dlml.tqdm'
        ):
            # Download data files
            dlml.download_data_files(version='v1.0.0', use_cache=False)

            # Verify _download_file was called for model_files.txt and each non-commented file
            assert mock_download.call_count == 5  # model_files.txt + 4 files


# --- Integration Tests ---


def test_cache_version_tracking_integration() -> None:
    """Test integration between cache version tracking and version checking."""
    # Mock cache with version metadata
    mock_cache = {
        'version': 'v1.0.0',
        'download_type': 'version',
        'commit_sha': '1234567',
        'last_updated': '2025-08-13T10:00:00',
    }

    # Mock GitHub API response with newer version
    mock_response = MagicMock()
    mock_response.json.return_value = {'tag_name': 'v1.1.0'}
    mock_response.raise_for_status.return_value = None

    with patch('pelicun.tools.dlml.load_cache', return_value=mock_cache), patch(
        'pelicun.tools.dlml.save_cache'
    ) as mock_save, patch('requests.get', return_value=mock_response), patch(
        'pelicun.tools.dlml.DLML_DATA_DIR', new=Path('/some/mock/dir')
    ), patch('pathlib.Path.exists', return_value=True):
        result = dlml.check_dlml_version()

        # Should use cached version info for comparison
        assert result['current_version'] == 'v1.0.0'
        assert result['latest_version'] == 'v1.1.0'
        assert result['update_available'] is True

        # Should update cache with version check results
        mock_save.assert_called_once()
        updated_cache = mock_save.call_args[0][1]
        assert updated_cache['current_version'] == 'v1.0.0'
        assert updated_cache['latest_version'] == 'v1.1.0'
        assert updated_cache['update_available'] is True


# These tests are not affected by the DLML_DATA_DIR refactoring.
def test_import_integration_success() -> None:
    """Test successful import integration with check_dlml_data."""
    # Mock successful data check
    with patch('pelicun.tools.dlml.check_dlml_data') as mock_check:
        mock_check.return_value = None

        # Import should succeed without raising exceptions
        try:
            # Simulate the import process
            from pelicun.tools.dlml import check_dlml_data  # noqa: PLC0415

            check_dlml_data()
        except (ImportError, RuntimeError, ValueError) as e:
            pytest.fail(f'Import integration should not raise exception: {e}')


# These tests are not affected by the DLML_DATA_DIR refactoring.
def test_import_integration_failure() -> None:
    """Test import integration handles check_dlml_data failures."""
    # Mock failed data check
    with patch(
        'pelicun.tools.dlml.check_dlml_data',
        side_effect=RuntimeError('Download failed'),
    ):
        # Should raise ImportError as would happen in __init__.py
        from pelicun.tools.dlml import check_dlml_data  # noqa: PLC0415

        with pytest.raises(RuntimeError, match='Download failed'):
            check_dlml_data()


def test_logging_configuration() -> None:
    """Test that the module-level logger is configured and used."""
    # Verify logger exists and is configured correctly.
    assert hasattr(dlml, 'logger')
    assert dlml.logger.name == 'pelicun.dlml'

    # Test that a function uses the logger by triggering a loggable event.
    # Here, we trigger a warning in _get_changed_files by mocking a network error.
    with patch(
        'requests.get', side_effect=requests.exceptions.RequestException
    ), patch.object(dlml.logger, 'warning') as mock_warning:
        dlml._get_changed_files('base', 'head', headers={})

        # Assert that the logger's warning method was called as expected.
        mock_warning.assert_called()


def test_warning_system_integration() -> None:
    """Test integration with pelicun warning system."""

    from pelicun.pelicun_warnings import PelicunWarning  # noqa: PLC0415

    # Mock version check result with update available
    mock_version_info = {
        'update_available': True,
        'current_version': 'v1.0.0',
        'latest_version': 'v1.1.0',
        'error': None,
    }

    with patch('pathlib.Path.exists', return_value=True), patch(
        'pathlib.Path.is_dir', return_value=True
    ), patch('pathlib.Path.iterdir', return_value=[MagicMock()]), patch(
        'pelicun.tools.dlml.check_dlml_version', return_value=mock_version_info
    ), patch('warnings.warn') as mock_warn, patch(
        'pelicun.tools.dlml.DLML_DATA_DIR', new=Path('/some/mock/dir')
    ):
        dlml.check_dlml_data()

        # Should use PelicunWarning class
        mock_warn.assert_called_once()
        args, kwargs = mock_warn.call_args
        assert len(args) >= 2
        assert args[1] == PelicunWarning

        # Should have appropriate stacklevel
        assert 'stacklevel' in kwargs
        assert kwargs['stacklevel'] == 2


# --- Command-Line Interface Integration Tests ---


def test_cli_integration_invalid_action() -> None:
    """Integration test: CLI with invalid action (not 'update')."""
    # Run the CLI with invalid action
    result = subprocess.run(  # noqa: S603
        ['pelicun', 'dlml', 'invalid'],  # noqa: S607
        capture_output=True,
        text=True,
        check=False,
    )

    # Verify exit code and output
    assert result.returncode == 2  # argparse returns 2 for invalid choice
    assert "invalid choice: 'invalid'" in result.stderr


def test_cli_integration_missing_arguments() -> None:
    """Integration test: CLI with insufficient arguments."""
    # Run the CLI with no arguments for dlml
    result = subprocess.run(  # noqa: S603
        ['pelicun', 'dlml'],  # noqa: S607
        capture_output=True,
        text=True,
        check=False,
    )

    # Verify exit code and output
    assert result.returncode == 2  # argparse returns 2 for missing required argument
    assert 'required' in result.stderr
