from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from codeflash_core import (
    FileDiffContent,
    InvalidAPIKeyError,
    PrComment,
)
from codeflash_core._http import _resolve_url_from_env
from codeflash_core._platform import (
    PlatformClient,
    parse_repo_owner_and_name,
)


@pytest.fixture(name="client")
def _client():
    """A PlatformClient pointed at localhost."""
    with PlatformClient(
        base_url="http://localhost:3001", api_key="cf-test"
    ) as c:
        yield c


@pytest.fixture(name="mock_post")
def _mock_post(client):
    """Patch client._session.post and return the mock."""
    with patch.object(client._session, "post") as mock:
        mock.return_value = MagicMock()
        mock.return_value.json.return_value = {}
        mock.return_value.ok = True
        mock.return_value.status_code = 200
        mock.return_value.text = ""
        yield mock


@pytest.fixture(name="mock_get")
def _mock_get(client):
    """Patch client._session.get and return the mock."""
    with patch.object(client._session, "get") as mock:
        mock.return_value = MagicMock()
        mock.return_value.ok = True
        mock.return_value.status_code = 200
        mock.return_value.text = "true"
        yield mock


class TestParseRepoOwnerAndName:
    """Tests for parse_repo_owner_and_name."""

    def test_https_with_git_suffix(self):
        """HTTPS URL with .git suffix is parsed correctly."""
        assert ("owner", "repo") == parse_repo_owner_and_name(
            "https://github.com/owner/repo.git"
        )

    def test_https_without_git_suffix(self):
        """HTTPS URL without .git suffix is parsed correctly."""
        assert ("owner", "repo") == parse_repo_owner_and_name(
            "https://github.com/owner/repo"
        )

    def test_ssh_with_git_suffix(self):
        """SSH URL with .git suffix is parsed correctly."""
        assert ("owner", "repo") == parse_repo_owner_and_name(
            "git@github.com:owner/repo.git"
        )

    def test_ssh_without_git_suffix(self):
        """SSH URL without .git suffix is parsed correctly."""
        assert ("owner", "repo") == parse_repo_owner_and_name(
            "git@github.com:owner/repo"
        )

    def test_trailing_slash(self):
        """Trailing slash is stripped before parsing."""
        assert ("owner", "repo") == parse_repo_owner_and_name(
            "https://github.com/owner/repo/"
        )

    def test_org_with_hyphens(self):
        """Org names with hyphens are preserved."""
        assert ("my-org", "my-repo") == parse_repo_owner_and_name(
            "git@github.com:my-org/my-repo.git"
        )


class TestResolveUrlFromEnv:
    """Tests for _resolve_url_from_env."""

    def test_returns_prod_by_default(self, monkeypatch):
        """Unset env var returns prod URL."""
        monkeypatch.delenv("MY_SERVER", raising=False)
        assert "https://prod.example.com" == _resolve_url_from_env(
            "MY_SERVER", "https://prod.example.com", "http://localhost:9000"
        )

    def test_returns_local_when_set(self, monkeypatch):
        """'local' env var returns local URL."""
        monkeypatch.setenv("MY_SERVER", "local")
        assert "http://localhost:9000" == _resolve_url_from_env(
            "MY_SERVER", "https://prod.example.com", "http://localhost:9000"
        )

    def test_case_insensitive(self, monkeypatch):
        """'LOCAL' (uppercase) returns local URL."""
        monkeypatch.setenv("MY_SERVER", "LOCAL")
        assert "http://localhost:9000" == _resolve_url_from_env(
            "MY_SERVER", "https://prod.example.com", "http://localhost:9000"
        )

    def test_non_local_returns_prod(self, monkeypatch):
        """Any value other than 'local' returns prod URL."""
        monkeypatch.setenv("MY_SERVER", "staging")
        assert "https://prod.example.com" == _resolve_url_from_env(
            "MY_SERVER", "https://prod.example.com", "http://localhost:9000"
        )


class TestPlatformClient:
    """Tests for PlatformClient construction."""

    def test_default_timeout(self, monkeypatch):
        """Default timeout is 60 seconds."""
        monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test")
        with PlatformClient() as c:
            assert 60.0 == c._timeout

    def test_strips_trailing_slash(self):
        """Trailing slashes are stripped from the base URL."""
        with PlatformClient(
            base_url="http://localhost:3001/", api_key="cf-test"
        ) as c:
            assert "http://localhost:3001" == c._base_url

    def test_sets_auth_header(self):
        """An API key sets the Authorization header."""
        with PlatformClient(api_key="cf-test-key") as c:
            assert "Bearer cf-test-key" == c._session.headers["Authorization"]

    def test_context_manager_closes_session(self, monkeypatch):
        """Exiting the context manager closes the session."""
        monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test")
        c = PlatformClient()
        with patch.object(c._session, "close") as mock_close, c:
            pass
        mock_close.assert_called_once()

    def test_missing_key_raises(self, monkeypatch):
        """Missing CODEFLASH_API_KEY raises InvalidAPIKeyError."""
        monkeypatch.delenv("CODEFLASH_API_KEY", raising=False)
        with pytest.raises(InvalidAPIKeyError, match="not found"):
            PlatformClient()


class TestCfapiUrlResolution:
    """Tests for CODEFLASH_CFAPI_SERVER URL resolution."""

    @pytest.mark.parametrize(
        ("env_value", "expected_url"),
        [
            ("local", "http://localhost:3001"),
            ("LOCAL", "http://localhost:3001"),
            ("prod", "https://app.codeflash.ai"),
            (None, "https://app.codeflash.ai"),
        ],
    )
    def test_env_resolution(self, monkeypatch, env_value, expected_url):
        """CODEFLASH_CFAPI_SERVER resolves to the correct URL."""
        monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test")
        if env_value is None:
            monkeypatch.delenv("CODEFLASH_CFAPI_SERVER", raising=False)
        else:
            monkeypatch.setenv("CODEFLASH_CFAPI_SERVER", env_value)
        with PlatformClient() as c:
            assert expected_url == c._base_url

    def test_explicit_url_overrides_env(self, monkeypatch):
        """An explicit base_url overrides the env var."""
        monkeypatch.setenv("CODEFLASH_CFAPI_SERVER", "local")
        with PlatformClient(
            base_url="https://custom.api", api_key="cf-test"
        ) as c:
            assert "https://custom.api" == c._base_url


class TestGetBlocklistedFunctions:
    """Tests for PlatformClient.get_blocklisted_functions."""

    def test_success(self, client, mock_post):
        """Successful response returns parsed blocklist."""
        mock_post.return_value.json.return_value = {
            "src/module.py": ["func_a()", "func_b()"],
            "src/other.py": ["helper()"],
        }
        result = client.get_blocklisted_functions("owner", "repo", 42)

        assert {
            "module.py": {"func_a", "func_b"},
            "other.py": {"helper"},
        } == result
        mock_post.assert_called_once()

    def test_server_error_returns_empty(self, client, mock_post):
        """A 500 response returns an empty dict."""
        mock_post.return_value.status_code = 500
        mock_post.return_value.ok = False
        result = client.get_blocklisted_functions("owner", "repo", 42)

        assert {} == result

    def test_error_key_returns_empty(self, client, mock_post):
        """A response with 'error' key returns empty dict."""
        mock_post.return_value.json.return_value = {"error": "not found"}
        result = client.get_blocklisted_functions("owner", "repo", 42)

        assert {} == result

    def test_not_found_returns_empty(self, client, mock_post):
        """A 404 response returns empty dict."""
        mock_post.return_value.status_code = 404
        mock_post.return_value.ok = False
        result = client.get_blocklisted_functions("owner", "repo", 42)

        assert {} == result


class TestIsFunctionBeingOptimizedAgain:
    """Tests for PlatformClient.is_function_being_optimized_again."""

    def test_success(self, client, mock_post):
        """Returns JSON response on success."""
        mock_post.return_value.json.return_value = {"is_optimized": True}
        result = client.is_function_being_optimized_again(
            "owner", "repo", 42, [{"code": "def f(): pass"}]
        )

        assert {"is_optimized": True} == result


class TestMarkOptimizationSuccess:
    """Tests for PlatformClient.mark_optimization_success."""

    def test_sends_correct_payload(self, client, mock_post):
        """Sends trace_id and is_optimization_found in payload."""
        client.mark_optimization_success(
            "trace-123", is_optimization_found=True
        )

        mock_post.assert_called_once()
        call_data = mock_post.call_args
        assert "/cfapi/mark-as-success" in call_data[0][0]


class TestSendCompletionEmail:
    """Tests for PlatformClient.send_completion_email."""

    def test_sends_correct_payload(self, client, mock_post):
        """Sends owner and repo in payload."""
        client.send_completion_email("owner", "repo")

        mock_post.assert_called_once()
        call_data = mock_post.call_args
        assert "/cfapi/send-completion-email" in call_data[0][0]


class TestIsGithubAppInstalled:
    """Tests for PlatformClient.is_github_app_installed."""

    def test_installed_returns_true(self, client, mock_get):
        """Returns True when response is ok and text is 'true'."""
        assert client.is_github_app_installed("owner", "repo") is True

    def test_not_installed_returns_false(self, client, mock_get):
        """Returns False when response text is not 'true'."""
        mock_get.return_value.text = "false"
        assert client.is_github_app_installed("owner", "repo") is False

    def test_error_returns_false(self, client, mock_get):
        """Returns False on error response."""
        mock_get.return_value.ok = False
        assert client.is_github_app_installed("owner", "repo") is False


class TestSuggestChanges:
    """Tests for PlatformClient.suggest_changes."""

    def test_serializes_payload(self, client, mock_post):
        """FileDiffContent is serialized with camelCase keys."""
        changes = {
            "file.py": FileDiffContent(old_content="old", new_content="new")
        }
        comment = PrComment(
            optimization_explanation="faster",
            best_runtime=100,
            original_runtime=200,
            function_name="f",
            relative_file_path="file.py",
            speedup_x="2x",
            speedup_pct="50%",
            loop_count=1,
            report_table={},
        )
        client.suggest_changes(
            "owner",
            "repo",
            42,
            changes,
            comment,
            "existing",
            "generated",
            "trace-1",
            "coverage",
        )

        mock_post.assert_called_once()
        url = mock_post.call_args[0][0]
        assert "/cfapi/suggest-pr-changes" in url


class TestCreatePr:
    """Tests for PlatformClient.create_pr."""

    def test_sends_to_correct_endpoint(self, client, mock_post):
        """POST goes to /cfapi/create-pr."""
        changes = {
            "file.py": FileDiffContent(old_content="old", new_content="new")
        }
        comment = PrComment(
            optimization_explanation="faster",
            best_runtime=100,
            original_runtime=200,
            function_name="f",
            relative_file_path="file.py",
            speedup_x="2x",
            speedup_pct="50%",
            loop_count=1,
            report_table={},
        )
        client.create_pr(
            "owner",
            "repo",
            "main",
            changes,
            comment,
            "existing",
            "generated",
            "trace-1",
            "coverage",
        )

        url = mock_post.call_args[0][0]
        assert "/cfapi/create-pr" in url


class TestSetupGithubActions:
    """Tests for PlatformClient.setup_github_actions."""

    def test_sends_to_correct_endpoint(self, client, mock_post):
        """POST goes to /cfapi/setup-github-actions."""
        client.setup_github_actions("owner", "repo", "main", "workflow yaml")

        url = mock_post.call_args[0][0]
        assert "/cfapi/setup-github-actions" in url


class TestSerializeFileChanges:
    """Tests for PlatformClient._serialize_file_changes."""

    def test_converts_to_camel_case(self):
        """Attrs fields are converted to camelCase API format."""
        changes = {
            "a.py": FileDiffContent(old_content="old_a", new_content="new_a"),
            "b.py": FileDiffContent(old_content="old_b", new_content="new_b"),
        }
        result = PlatformClient._serialize_file_changes(changes)

        assert {
            "a.py": {
                "oldContent": "old_a",
                "newContent": "new_a",
            },
            "b.py": {
                "oldContent": "old_b",
                "newContent": "new_b",
            },
        } == result
