"""Comprehensive tests for code object local analysis.

Tests cover:
- Local analysis when tools available
- Tool availability detection
- Error handling and detailed error messages
- Fallback behavior
- Edge cases
"""

import os
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest

from wafer_core.lib.kernel_scope.code_object import (
    can_analyze_locally,
    check_installation,
    analyze_code_object_local,
    _check_llvm_rocm_available,
    _check_local_is_amd,
    _find_rocm_llvm_bin,
    _get_binary_path,
    ISACheckResult,
)
from wafer_core.lib.kernel_scope.api import analyze_code_object


class TestToolAvailability:
    """Tests for tool availability detection."""

    def test_check_installation_returns_detailed_result(self) -> None:
        """check_installation should return ISACheckResult with all diagnostics."""
        result = check_installation()

        assert isinstance(result, ISACheckResult)
        assert hasattr(result, "available")
        assert hasattr(result, "checked_paths")
        assert hasattr(result, "missing_tools")
        assert isinstance(result.available, bool)

    def test_check_llvm_rocm_checks_all_required_tools(self) -> None:
        """Should check for all three required tools."""
        result = _check_llvm_rocm_available()

        required_tools = ["llvm-objdump", "llvm-readelf", "clang-offload-bundler"]

        if not result.available:
            assert result.missing_tools is not None
            # All required tools should be in missing list if unavailable
            for tool in required_tools:
                assert tool in result.missing_tools

    def test_check_local_is_amd_checks_multiple_methods(self) -> None:
        """Should check multiple methods for AMD GPU detection."""
        is_amd, gpu_name, checked_methods = _check_local_is_amd()

        assert isinstance(is_amd, bool)
        assert isinstance(checked_methods, list)
        assert len(checked_methods) > 0

        # Should have checked at least one method
        expected_methods = ["rocm-smi", "rocminfo", "/dev/kfd"]
        assert any(method in checked_methods for method in expected_methods)

    def test_can_analyze_locally_checks_all_tools(self) -> None:
        """can_analyze_locally should verify all three tools exist."""
        # This uses actual system state
        result = can_analyze_locally()

        assert isinstance(result, bool)

        # If False, detailed check should show what's missing
        if not result:
            check_result = check_installation()
            assert check_result.missing_tools is not None
            assert len(check_result.missing_tools) > 0


class TestPathDiscovery:
    """Tests for dynamic path discovery."""

    def test_find_rocm_llvm_bin_checks_multiple_locations(self) -> None:
        """Should check environment variable, system path, user path, and PATH."""
        # Test with no tools available (most common case)
        result = _find_rocm_llvm_bin()

        # Should return None if not found, but shouldn't crash
        assert result is None or isinstance(result, Path)

    def test_environment_variable_takes_precedence(self) -> None:
        """ROCM_LLVM_BIN environment variable should be checked first."""
        custom_path = "/custom/rocm/llvm/bin"

        with patch.dict(os.environ, {"ROCM_LLVM_BIN": custom_path}):
            with patch("pathlib.Path.exists", return_value=True):
                result = _find_rocm_llvm_bin()

                # Should return the custom path if it exists
                if result:
                    assert str(result) == custom_path

    def test_system_path_checked_second(self) -> None:
        """System path /opt/rocm/llvm/bin should be checked second."""
        with patch.dict(os.environ, {}, clear=True):
            # Patch Path.exists to return True for system path and llvm-objdump
            original_exists = Path.exists
            
            def mock_exists(self):
                path_str = str(self)
                if path_str == "/opt/rocm/llvm/bin":
                    return True
                if path_str.endswith("llvm-objdump"):
                    return True
                return original_exists(self)
            
            with patch.object(Path, "exists", mock_exists):
                result = _find_rocm_llvm_bin()

                # Should find system path
                if result:
                    assert "/opt/rocm" in str(result)

    def test_user_path_checked_third(self) -> None:
        """User path ~/.local/rocm/llvm/bin should be checked third."""
        with patch.dict(os.environ, {}, clear=True):
            # Patch Path.exists to return True for user path and llvm-objdump
            original_exists = Path.exists
            
            def mock_exists(self):
                path_str = str(self)
                if ".local/rocm" in path_str or "~/.local/rocm/llvm/bin" in path_str:
                    return True
                if path_str.endswith("llvm-objdump"):
                    return True
                return original_exists(self)
            
            with patch.object(Path, "exists", mock_exists):
                result = _find_rocm_llvm_bin()

                # Should find user path
                if result:
                    assert ".local" in str(result) or "rocm" in str(result).lower()

    def test_path_fallback_works(self) -> None:
        """Should fall back to PATH if tools found there."""
        with patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/bin/llvm-objdump"

            result = _find_rocm_llvm_bin()

            # Should find tool in PATH
            if result:
                assert result is not None

    def test_get_binary_path_raises_on_missing(self) -> None:
        """_get_binary_path should raise RuntimeError if binary not found."""
        with patch("shutil.which", return_value=None):
            with patch("pathlib.Path.exists", return_value=False):
                with pytest.raises(RuntimeError, match="Binary not found"):
                    _get_binary_path("nonexistent-tool")


class TestLocalAnalysis:
    """Tests for local analysis execution."""

    def test_analyze_code_object_local_requires_tools(self) -> None:
        """Should raise RuntimeError if tools not available."""
        with tempfile.NamedTemporaryFile(suffix=".co", delete=False) as f:
            f.write(b"dummy")
            co_path = Path(f.name)

        try:
            with patch(
                "wafer_core.lib.kernel_scope.code_object.can_analyze_locally",
                return_value=False,
            ):
                with pytest.raises(RuntimeError, match="ROCm LLVM tools not available"):
                    analyze_code_object_local(co_path)
        finally:
            os.unlink(co_path)

    def test_analyze_code_object_local_validates_file(self) -> None:
        """Should validate file exists and is readable."""
        fake_path = Path("/nonexistent/file.co")

        with patch(
            "wafer_core.lib.kernel_scope.code_object.can_analyze_locally",
            return_value=True,
        ):
            with pytest.raises(FileNotFoundError):
                analyze_code_object_local(fake_path)


class TestErrorMessages:
    """Tests for error message format and content."""

    def test_error_message_structure(self) -> None:
        """Error messages should follow expected structure."""
        check_result = _check_llvm_rocm_available()

        if not check_result.available:
            # Build error message
            error_lines = [
                "Error: Cannot analyze .co files - ROCm LLVM tools not found",
                "",
                "We checked:",
            ]

            if check_result.checked_paths:
                for path in check_result.checked_paths:
                    error_lines.append(f"  ✗ {path}")

            if check_result.missing_tools:
                error_lines.append("")
                error_lines.append("Missing tools:")
                for tool in check_result.missing_tools:
                    error_lines.append(f"  ✗ {tool}")

            error_lines.append("")
            error_lines.append("To fix, either:")

            error_msg = "\n".join(error_lines)

            # Verify structure
            assert "Error: Cannot analyze .co files" in error_msg
            assert "We checked:" in error_msg
            assert "To fix" in error_msg

    def test_error_message_includes_all_checked_paths(self) -> None:
        """Error messages should list all checked paths."""
        check_result = _check_llvm_rocm_available()

        if not check_result.available:
            assert check_result.checked_paths is not None

            error_msg = "\n".join(check_result.checked_paths or [])

            # Should include standard paths
            assert any("/opt/rocm" in path or "PATH" in path for path in check_result.checked_paths or [])

    def test_error_message_includes_missing_tools(self) -> None:
        """Error messages should list all missing tools."""
        check_result = _check_llvm_rocm_available()

        if not check_result.available:
            assert check_result.missing_tools is not None
            assert len(check_result.missing_tools) > 0

            error_msg = " ".join(check_result.missing_tools)

            # Should include required tools
            required = ["llvm-objdump", "llvm-readelf", "clang-offload-bundler"]
            for tool in required:
                if tool in check_result.missing_tools:
                    assert tool in error_msg

    def test_error_message_includes_amd_gpu_info(self) -> None:
        """Error messages should include AMD GPU detection info when relevant."""
        is_amd, gpu_name, checked_methods = _check_local_is_amd()

        if not is_amd:
            # Should note that GPU detection is optional
            note = f"Note: AMD GPU not detected locally (checked: {', '.join(checked_methods)})"
            assert len(checked_methods) > 0


class TestAPIFallback:
    """Tests for API fallback behavior."""

    def test_fallback_when_no_local_tools(self) -> None:
        """Should fall back to API when local tools unavailable."""
        with tempfile.NamedTemporaryFile(suffix=".co", delete=False) as f:
            f.write(b"dummy")
            co_path = Path(f.name)

        try:
            with patch(
                "wafer_core.lib.kernel_scope.code_object.can_analyze_locally",
                return_value=False,
            ):
                with patch("httpx.Client") as mock_client:
                    mock_response = MagicMock()
                    mock_response.json.return_value = {
                        "kernel_name": "test",
                        "architecture": "gfx942",
                        "vgpr_count": 32,
                        "sgpr_count": 24,
                        "agpr_count": 0,
                        "vgpr_spill_count": 0,
                        "sgpr_spill_count": 0,
                        "lds_bytes": 0,
                        "global_loads": 0,
                        "global_stores": 0,
                        "lds_ops": 0,
                        "mfma_count": 0,
                        "fma_count": 0,
                        "packed_ops_count": 0,
                        "waitcnt_full_stalls": 0,
                        "barriers": 0,
                        "isa_text": "",
                        "metadata_yaml": "",
                        "annotated_isa_text": "",
                    }
                    mock_response.raise_for_status = MagicMock()
                    mock_client.return_value.__enter__.return_value.post.return_value = (
                        mock_response
                    )

                    result = analyze_code_object(
                        co_path,
                        api_url="https://api.test",
                        auth_headers={"Authorization": "Bearer token"},
                    )

                    assert result.success
        finally:
            os.unlink(co_path)

    def test_detailed_error_when_no_tools_no_api(self) -> None:
        """Should provide detailed error when no tools and no API credentials."""
        with tempfile.NamedTemporaryFile(suffix=".co", delete=False) as f:
            f.write(b"dummy")
            co_path = Path(f.name)

        try:
            with patch(
                "wafer_core.lib.kernel_scope.code_object.can_analyze_locally",
                return_value=False,
            ):
                result = analyze_code_object(co_path)

                assert not result.success
                assert "Error: Cannot analyze .co files" in result.error
                assert "We checked:" in result.error
                assert "Missing tools:" in result.error
                assert "To fix" in result.error
        finally:
            os.unlink(co_path)

    def test_api_error_handling(self) -> None:
        """Should handle API errors gracefully."""
        import httpx

        with tempfile.NamedTemporaryFile(suffix=".co", delete=False) as f:
            f.write(b"dummy")
            co_path = Path(f.name)

        try:
            with patch(
                "wafer_core.lib.kernel_scope.code_object.can_analyze_locally",
                return_value=False,
            ):
                with patch("httpx.Client") as mock_client:
                    # Simulate 503 error from API
                    mock_response = MagicMock()
                    mock_response.status_code = 503
                    mock_response.text = "Service Unavailable"
                    mock_error = httpx.HTTPStatusError(
                        "Service Unavailable", request=MagicMock(), response=mock_response
                    )

                    mock_client.return_value.__enter__.return_value.post.side_effect = (
                        mock_error
                    )

                    result = analyze_code_object(
                        co_path,
                        api_url="https://api.test",
                        auth_headers={"Authorization": "Bearer token"},
                    )

                    assert not result.success
                    # Error should indicate API fallback was attempted
                    error_msg = result.error
                    assert (
                        "API fallback also failed" in error_msg
                        or "API error (503)" in error_msg
                        or "503" in error_msg
                        or "Service Unavailable" in error_msg
                    )
        finally:
            os.unlink(co_path)


class TestEdgeCases:
    """Tests for edge cases and error conditions."""

    def test_file_not_found_error(self) -> None:
        """Should return clear error for missing file."""
        fake_path = Path("/nonexistent/file.co")

        result = analyze_code_object(fake_path)

        assert not result.success
        assert "File not found" in result.error

    def test_wrong_file_extension(self) -> None:
        """Should return clear error for wrong file extension."""
        with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
            f.write(b"dummy")
            txt_path = Path(f.name)

        try:
            result = analyze_code_object(txt_path)

            assert not result.success
            assert "Expected .co file" in result.error
        finally:
            os.unlink(txt_path)

    def test_empty_file(self) -> None:
        """Should handle empty file gracefully."""
        with tempfile.NamedTemporaryFile(suffix=".co", delete=False) as f:
            f.write(b"")
            empty_path = Path(f.name)

        try:
            with patch(
                "wafer_core.lib.kernel_scope.code_object.can_analyze_locally",
                return_value=True,
            ):
                # Should either succeed with empty result or fail with clear error
                result = analyze_code_object(empty_path)

                # Result should be well-formed even if unsuccessful
                assert hasattr(result, "success")
                assert hasattr(result, "error")
        finally:
            os.unlink(empty_path)
