"""Tests for SBOM generation fixes with HuggingFace URLs and downloaded content.

This module tests the fix for FileNotFoundError when generating SBOMs for content
downloaded from URLs (HuggingFace, cloud storage, etc.).
"""

import json
from unittest.mock import patch

import pytest
from click.testing import CliRunner

from modelaudit.cli import cli
from modelaudit.integrations.sbom_generator import generate_sbom_pydantic


def create_mock_scan_result(
    bytes_scanned=1024, issues=None, files_scanned=1, assets=None, has_errors=False, scanners=None
):
    """Create a mock scan result for testing."""
    from modelaudit.models import create_initial_audit_result

    result = create_initial_audit_result()
    result.bytes_scanned = bytes_scanned
    result.files_scanned = files_scanned
    result.has_errors = has_errors
    if issues:
        result.issues = issues
    if assets:
        result.assets = assets
    if scanners:
        result.scanner_names = scanners
    return result


class TestSBOMURLFixes:
    """Test SBOM generation with URLs and downloaded content."""

    def test_sbom_with_huggingface_file_url_success(self, tmp_path):
        """Test SBOM generation after downloading HuggingFace file URL."""
        # Create a test file that simulates downloaded content
        downloaded_file = tmp_path / "pytorch_model.bin"
        downloaded_file.write_bytes(b"dummy model content for SBOM test")

        # Create mock scan result
        scan_result = create_mock_scan_result(
            bytes_scanned=len(b"dummy model content for SBOM test"),
            files_scanned=1,
            has_errors=False,
            scanners=["test_scanner"],
        )

        # Test SBOM generation with the downloaded file path (not the URL)
        sbom_json = generate_sbom_pydantic([str(downloaded_file)], scan_result)
        sbom_data = json.loads(sbom_json)

        # Verify SBOM structure
        assert sbom_data["bomFormat"] == "CycloneDX"
        assert sbom_data["specVersion"] == "1.6"
        assert "components" in sbom_data
        assert len(sbom_data["components"]) == 1

        component = sbom_data["components"][0]
        assert component["name"] == "pytorch_model.bin"
        assert component["type"] == "machine-learning-model"  # .bin files are ML models
        assert "hashes" in component
        assert len(component["hashes"]) == 1

    def test_sbom_with_huggingface_model_url_success(self, tmp_path):
        """Test SBOM generation after downloading HuggingFace model URL."""
        # Create a test directory that simulates downloaded model
        model_dir = tmp_path / "downloaded_model"
        model_dir.mkdir()
        (model_dir / "config.json").write_text('{"model_type": "bert"}')
        (model_dir / "pytorch_model.bin").write_bytes(b"model weights")
        (model_dir / "tokenizer.json").write_text('{"vocab": {}}')

        # Create mock scan result for directory
        scan_result = create_mock_scan_result(
            bytes_scanned=100, files_scanned=3, has_errors=False, scanners=["test_scanner"]
        )

        # Test SBOM generation with the downloaded directory path
        sbom_json = generate_sbom_pydantic([str(model_dir)], scan_result)
        sbom_data = json.loads(sbom_json)

        # Verify SBOM structure for directory
        assert sbom_data["bomFormat"] == "CycloneDX"
        assert len(sbom_data["components"]) == 3  # Three files in directory

        # Check that all files are included
        component_names = {comp["name"] for comp in sbom_data["components"]}
        expected_names = {"config.json", "pytorch_model.bin", "tokenizer.json"}
        assert component_names == expected_names

    def test_sbom_with_cloud_storage_url_success(self, tmp_path):
        """Test SBOM generation after downloading from cloud storage."""
        # Simulate downloaded content from cloud storage
        downloaded_file = tmp_path / "model.pkl"
        downloaded_file.write_bytes(b"pickled model data")

        scan_result = create_mock_scan_result(
            bytes_scanned=len(b"pickled model data"), files_scanned=1, has_errors=False
        )

        # Test SBOM generation
        sbom_json = generate_sbom_pydantic([str(downloaded_file)], scan_result)
        sbom_data = json.loads(sbom_json)

        assert len(sbom_data["components"]) == 1
        component = sbom_data["components"][0]
        assert component["name"] == "model.pkl"
        assert component["type"] == "machine-learning-model"

    def test_sbom_with_mixed_local_and_url_inputs(self, tmp_path):
        """Test SBOM generation with both local files and downloaded content."""
        # Create local file
        local_file = tmp_path / "local_model.onnx"
        local_file.write_bytes(b"local model")

        # Create downloaded file (simulating URL download)
        downloaded_file = tmp_path / "downloaded_model.safetensors"
        downloaded_file.write_bytes(b"downloaded model")

        scan_result = create_mock_scan_result(bytes_scanned=200, files_scanned=2, has_errors=False)

        # Test SBOM generation with both paths
        paths = [str(local_file), str(downloaded_file)]
        sbom_json = generate_sbom_pydantic(paths, scan_result)
        sbom_data = json.loads(sbom_json)

        assert len(sbom_data["components"]) == 2
        component_names = {comp["name"] for comp in sbom_data["components"]}
        expected_names = {"local_model.onnx", "downloaded_model.safetensors"}
        assert component_names == expected_names

    @pytest.mark.integration
    @patch("modelaudit.cli.is_huggingface_file_url")
    @patch("modelaudit.cli.download_file_from_hf")
    @patch("modelaudit.cli.scan_model_directory_or_file")
    @patch("modelaudit.cli.should_show_spinner", return_value=False)
    def test_cli_sbom_with_huggingface_file_url(
        self, mock_spinner, mock_scan, mock_download, mock_is_hf_file_url, tmp_path
    ):
        """Test CLI SBOM generation with HuggingFace file URL."""
        # Setup mocks
        mock_is_hf_file_url.return_value = True
        downloaded_file = tmp_path / "model.bin"
        downloaded_file.write_bytes(b"test model content")
        mock_download.return_value = downloaded_file

        mock_scan.return_value = create_mock_scan_result(bytes_scanned=100, files_scanned=1, has_errors=False)

        # Test CLI with SBOM output
        sbom_output = tmp_path / "test.sbom.json"
        runner = CliRunner()
        result = runner.invoke(
            cli,
            [
                "scan",
                "--no-cache",
                "--quiet",
                "--sbom",
                str(sbom_output),
                "https://huggingface.co/test/model/resolve/main/model.bin",
            ],
        )

        # Should succeed
        assert result.exit_code == 0, f"CLI failed: {result.output}\n{result.exception}"

        # SBOM file should be created
        assert sbom_output.exists()

        # Verify SBOM content
        sbom_data = json.loads(sbom_output.read_text())
        assert sbom_data["bomFormat"] == "CycloneDX"
        assert len(sbom_data["components"]) == 1
        assert sbom_data["components"][0]["name"] == "model.bin"

        # Verify download and scan were called correctly
        mock_download.assert_called_once()
        mock_scan.assert_called_once()
        # Verify scan was called with downloaded path, not URL
        assert mock_scan.call_args[0][0] == str(downloaded_file)

    @pytest.mark.integration
    @patch("modelaudit.cli.is_huggingface_url")
    @patch("modelaudit.cli.is_huggingface_file_url", return_value=False)
    @patch("modelaudit.cli.download_model")
    @patch("modelaudit.cli.scan_model_directory_or_file")
    @patch("modelaudit.cli.should_show_spinner", return_value=False)
    def test_cli_sbom_with_huggingface_model_url(
        self, mock_spinner, mock_scan, mock_download, mock_is_hf_file_url, mock_is_hf_url, tmp_path
    ):
        """Test CLI SBOM generation with HuggingFace model URL."""
        # Setup mocks
        mock_is_hf_url.return_value = True
        downloaded_dir = tmp_path / "model"
        downloaded_dir.mkdir()
        (downloaded_dir / "config.json").write_text("{}")
        (downloaded_dir / "model.bin").write_bytes(b"model")
        mock_download.return_value = downloaded_dir

        mock_scan.return_value = create_mock_scan_result(bytes_scanned=200, files_scanned=2, has_errors=False)

        # Test CLI with SBOM output
        sbom_output = tmp_path / "model.sbom.json"
        runner = CliRunner()
        result = runner.invoke(cli, ["scan", "--no-cache", "--quiet", "--sbom", str(sbom_output), "hf://test/model"])

        # Should succeed
        assert result.exit_code == 0, f"CLI failed: {result.output}\n{result.exception}"
        assert sbom_output.exists()

        # Verify SBOM content has components from directory
        sbom_data = json.loads(sbom_output.read_text())
        assert len(sbom_data["components"]) == 2
        component_names = {comp["name"] for comp in sbom_data["components"]}
        assert "config.json" in component_names
        assert "model.bin" in component_names

    @pytest.mark.integration
    @patch("modelaudit.cli.is_cloud_url")
    @patch("modelaudit.cli.is_huggingface_file_url", return_value=False)
    @patch("modelaudit.cli.is_huggingface_url", return_value=False)
    @patch("modelaudit.cli.download_from_cloud")
    @patch("modelaudit.cli.scan_model_directory_or_file")
    @patch("modelaudit.cli.should_show_spinner", return_value=False)
    def test_cli_sbom_with_cloud_url(
        self, mock_spinner, mock_scan, mock_download, mock_is_hf_url, mock_is_hf_file_url, mock_is_cloud_url, tmp_path
    ):
        """Test CLI SBOM generation with cloud storage URL."""
        # Setup mocks
        mock_is_cloud_url.return_value = True
        downloaded_file = tmp_path / "cloud_model.pkl"
        downloaded_file.write_bytes(b"cloud model data")
        mock_download.return_value = downloaded_file

        mock_scan.return_value = create_mock_scan_result(bytes_scanned=150, files_scanned=1, has_errors=False)

        # Test CLI with SBOM
        sbom_output = tmp_path / "cloud.sbom.json"
        runner = CliRunner()
        result = runner.invoke(
            cli, ["scan", "--no-cache", "--quiet", "--sbom", str(sbom_output), "s3://bucket/model.pkl"]
        )

        assert result.exit_code == 0, f"CLI failed: {result.output}\n{result.exception}"
        assert sbom_output.exists()

        sbom_data = json.loads(sbom_output.read_text())
        assert len(sbom_data["components"]) == 1
        assert sbom_data["components"][0]["name"] == "cloud_model.pkl"

    def test_sbom_file_not_found_error_prevention(self, tmp_path):
        """Test that SBOM generation handles URLs gracefully (may succeed or fail)."""
        # This test documents the behavior - URLs might work depending on SBOM implementation
        url = "https://huggingface.co/test/model/resolve/main/file.bin"

        # Create a mock scan result
        scan_result = create_mock_scan_result()

        # The SBOM implementation may handle URLs gracefully or raise FileNotFoundError
        # The important thing is that the CLI fix ensures only file paths reach SBOM generation
        try:
            sbom_json = generate_sbom_pydantic([url], scan_result)
            # If it succeeds, that's fine - some SBOM implementations are robust
            assert isinstance(sbom_json, str)
        except FileNotFoundError:
            # If it fails, that's also expected for URLs that don't exist as files
            pass

    def test_sbom_with_nonexistent_local_file_handling(self, tmp_path):
        """Test SBOM generation gracefully handles nonexistent files."""
        nonexistent_file = tmp_path / "missing.pkl"
        # Note: file doesn't exist

        scan_result = create_mock_scan_result()

        # Should not crash, but may have empty hashes
        sbom_json = generate_sbom_pydantic([str(nonexistent_file)], scan_result)
        sbom_data = json.loads(sbom_json)

        assert len(sbom_data["components"]) == 1
        component = sbom_data["components"][0]
        # For nonexistent files, hashes field may be empty or missing
        if "hashes" in component:
            # If hashes field is present, it should be a list (may be empty)
            assert isinstance(component["hashes"], list)

    @pytest.mark.integration
    @patch("modelaudit.cli.is_huggingface_url")
    @patch("modelaudit.cli.is_huggingface_file_url", return_value=False)
    @patch("modelaudit.cli.download_model")
    @patch("modelaudit.cli.scan_model_directory_or_file")
    @patch("modelaudit.cli.should_show_spinner", return_value=False)
    def test_cli_sbom_with_download_failure(
        self, mock_spinner, mock_scan, mock_download, mock_is_hf_file_url, mock_is_hf_url, tmp_path
    ):
        """Test CLI behavior when download fails but SBOM is requested."""
        # Setup mocks for download failure
        mock_is_hf_url.return_value = True
        mock_download.side_effect = Exception("Download failed")

        sbom_output = tmp_path / "failed.sbom.json"
        runner = CliRunner()
        result = runner.invoke(
            cli, ["scan", "--no-cache", "--quiet", "--sbom", str(sbom_output), "hf://test/failing-model"]
        )

        # Should handle the error gracefully
        assert result.exit_code != 0  # Should fail due to download error
        # SBOM file should not be created when no successful scans occurred
        # (This is expected behavior - no scanned content means no SBOM)

    def test_sbom_cross_platform_file_paths(self, tmp_path):
        """Test SBOM generation works with different file path formats (Windows/Unix)."""
        # Create test files with different path characteristics
        files = [
            tmp_path / "simple.pkl",
            tmp_path / "file with spaces.bin",
            tmp_path / "unicode_文件.onnx",
        ]

        for file_path in files:
            file_path.write_bytes(b"test content")

        scan_result = create_mock_scan_result(files_scanned=len(files))

        # Test SBOM generation with all file types
        file_paths = [str(f) for f in files]
        sbom_json = generate_sbom_pydantic(file_paths, scan_result)
        sbom_data = json.loads(sbom_json)

        assert len(sbom_data["components"]) == len(files)

        # Verify all components have valid hashes (indicating successful file access)
        for component in sbom_data["components"]:
            assert "hashes" in component
            assert len(component["hashes"]) == 1
            assert component["hashes"][0]["alg"] == "SHA-256"
            assert len(component["hashes"][0]["content"]) == 64  # SHA-256 hex length

    @pytest.mark.parametrize("python_version", ["3.9", "3.12"])
    def test_sbom_python_version_compatibility(self, tmp_path, python_version):
        """Test that SBOM generation works across Python versions."""
        # This is more of a smoke test - actual version testing happens in CI
        test_file = tmp_path / f"model_py{python_version.replace('.', '_')}.pkl"
        test_file.write_bytes(b"version test content")

        scan_result = create_mock_scan_result()

        # Should work regardless of Python version
        sbom_json = generate_sbom_pydantic([str(test_file)], scan_result)
        sbom_data = json.loads(sbom_json)

        assert sbom_data["bomFormat"] == "CycloneDX"
        assert sbom_data["specVersion"] == "1.6"
        assert len(sbom_data["components"]) == 1

    def test_sbom_large_file_handling(self, tmp_path):
        """Test SBOM generation with larger files (simulating real model files)."""
        # Create a larger test file (1MB)
        large_file = tmp_path / "large_model.bin"
        large_content = b"x" * (1024 * 1024)  # 1MB of data
        large_file.write_bytes(large_content)

        scan_result = create_mock_scan_result(bytes_scanned=len(large_content), files_scanned=1)

        # Should handle large files without issues
        sbom_json = generate_sbom_pydantic([str(large_file)], scan_result)
        sbom_data = json.loads(sbom_json)

        assert len(sbom_data["components"]) == 1
        component = sbom_data["components"][0]

        # Verify file size is recorded correctly
        properties = {prop["name"]: prop["value"] for prop in component.get("properties", [])}
        assert "size" in properties
        assert int(properties["size"]) == len(large_content)
