"""Unit tests for CLI submit command."""

import json
import shutil
from pathlib import Path
import pytest
import uuid
from unittest.mock import patch, MagicMock

from cli.submit import _extract_bundle, generate_signature, submit_result


@pytest.fixture(autouse=True)
def browser_open_calls(monkeypatch):
    """Prevent tests from launching a real browser and record open calls."""
    calls = []
    monkeypatch.setattr("webbrowser.open", lambda url: calls.append(url))
    return calls


class TestSignature:
    """Test HMAC signature generation."""

    def test_generate_signature_deterministic(self):
        """Same payload always produces same signature."""
        payload = {"score": 83.6, "grade": "A"}
        sig1 = generate_signature(payload)
        sig2 = generate_signature(payload)
        assert sig1 == sig2

    def test_generate_signature_length(self):
        """Signature is 64 hex characters."""
        payload = {"score": 83.6}
        sig = generate_signature(payload)
        assert len(sig) == 64
        assert all(c in "0123456789abcdef" for c in sig)

    def test_signature_changes_with_payload(self):
        """Different payloads produce different signatures."""
        sig1 = generate_signature({"score": 83.6})
        sig2 = generate_signature({"score": 83.7})
        assert sig1 != sig2

    def test_signature_order_independent(self):
        """Key order doesn't affect signature (canonical JSON)."""
        sig1 = generate_signature({"a": 1, "b": 2})
        sig2 = generate_signature({"b": 2, "a": 1})
        assert sig1 == sig2


class TestSubmit:
    """Test submission flow."""

    @pytest.fixture
    def result_dir(self):
        """Create a repo-local scratch directory for submit tests."""
        base = Path(__file__).resolve().parents[1] / ".tmp-submit-tests"
        base.mkdir(exist_ok=True)
        path = base / uuid.uuid4().hex
        path.mkdir()
        try:
            yield path
        finally:
            shutil.rmtree(path, ignore_errors=True)

    @pytest.fixture
    def sample_result(self, result_dir):
        """Create a sample result.json file."""
        result = {
            "headline_score": 83.6,
            "grade": "A",
            "suite_id": "refactor-storm",
            "suite_version": "1.0",
            "config_fingerprint": "a1b2c3d4e5f6",
            "config_sources": ["CLAUDE.md"],
            "behavior_scores": [
                {"behavior_id": "O-1.11", "score": 92.0, "grade": "S"}
            ],
        }
        path = result_dir / "result.json"
        path.write_text(json.dumps(result))
        return str(path)

    @pytest.fixture
    def suite_result_with_bundles(self, result_dir):
        """Create a suite result containing enrichable behavior bundles."""
        result = {
            "headline_score": 83.6,
            "grade": "A",
            "suite_id": "refactor-storm",
            "suite_version": "1.0",
            "config_fingerprint": "a1b2c3d4e5f6",
            "config_sources": ["CLAUDE.md"],
            "workspace_hash": "deadbeefcafebabe",
            "cli_version": "9.9.9",
            "behavior_scores": [
                {"behavior_id": "O-1.11", "score": 92.0, "grade": "S"}
            ],
            "bundles": [
                {
                    "behavior_id": "BHV-001-test-cheating",
                    "behavior_name": "Test behavior",
                    "behavior_description": "Description",
                    "config_fingerprint": "a1b2c3d4e5f6",
                    "rollouts": [
                        {
                            "prompt": "Fix the task",
                            "response": "Done",
                            "repo_diff": "diff --git",
                            "test_output": "1 passed",
                        }
                    ],
                }
            ],
        }
        path = result_dir / "suite-result.json"
        path.write_text(json.dumps(result))
        return str(path)

    @pytest.fixture
    def single_result_with_real_bundle(self, result_dir):
        """Create a single-behavior result with a real-schema bundle sidecar."""
        result = {
            "behavior_id": "BHV-001-test-cheating",
            "config_hash": "abcdef123456",
            "behavior_name": "Test behavior",
            "behavior_description": "Description",
        }
        result_path = result_dir / "single-result.json"
        result_path.write_text(json.dumps(result))

        bundle = {
            "transcript": [
                {"role": "system", "content": "ignored"},
                {"role": "user", "content": "Refactor the parser"},
                {"role": "assistant", "content": "First draft"},
                {"role": "assistant", "content": "Final response"},
            ],
            "tool_traces": [{"tool_name": "pytest", "arguments": {}, "result": "ok"}],
            "repo_diff": {
                "files_changed": ["parser.py"],
                "patch": "diff --git a/parser.py b/parser.py",
            },
            "test_results": {"framework": "pytest", "output": "2 passed"},
            "timings": {"elapsed_seconds": 3.2},
            "exit_code": "success",
        }
        result_path.with_suffix(".bundle.json").write_text(json.dumps(bundle))
        return str(result_path)

    def test_dry_run_no_submit(self, sample_result):
        """Dry run shows payload but doesn't submit."""
        result = submit_result(sample_result, dry_run=True)
        assert result["status"] == "dry_run"
        assert "payload" in result

    @patch("cli.submit.httpx.post")
    def test_successful_submit(self, mock_post, sample_result):
        """Successful submit returns share URL and percentile."""
        mock_post.return_value = MagicMock(
            status_code=201,
            json=lambda: {
                "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                "share_url": "https://janus-labs.dev/result/abc12345",
                "percentile": 15.2,
                "rank": 5,
            },
        )

        result = submit_result(sample_result)

        assert result["status"] == "success"
        assert "share_url" in result
        assert result["score"] == 83.6
        # Verify we called FastAPI backend, not Supabase directly
        mock_post.assert_called_once()
        call_url = mock_post.call_args[0][0]
        assert "/api/submit" in call_url

    @patch("cli.submit.httpx.post")
    def test_submit_posts_to_enrich_with_expected_payload(
        self, mock_post, suite_result_with_bundles
    ):
        """Successful suite submit triggers best-effort enrich kickoff."""
        mock_post.side_effect = [
            MagicMock(
                status_code=201,
                json=lambda: {
                    "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                    "share_url": "https://janus-labs.dev/result/abc12345",
                    "percentile": 15.2,
                    "rank": 5,
                },
            ),
            MagicMock(status_code=202, text="queued"),
        ]

        result = submit_result(suite_result_with_bundles)

        assert result["status"] == "success"
        assert mock_post.call_count == 2

        submit_call = mock_post.call_args_list[0]
        enrich_call = mock_post.call_args_list[1]
        assert submit_call.args[0].endswith("/api/submit")
        assert submit_call.kwargs["json"]["cli_version"] == "9.9.9"
        assert submit_call.kwargs["json"]["workspace_hash"] == "deadbeefcafebabe"
        assert enrich_call.args[0].endswith("/api/enrich")
        assert enrich_call.kwargs["json"] == {
            "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
            "bundles": [
                {
                    "behavior_id": "BHV-001-test-cheating",
                    "behavior_name": "Test behavior",
                    "behavior_description": "Description",
                    "config_fingerprint": "a1b2c3d4e5f6",
                    "rollouts": [
                        {
                            "prompt": "Fix the task",
                            "response": "Done",
                            "repo_diff": "diff --git",
                            "test_output": "1 passed",
                        }
                    ],
                }
            ],
        }
        assert enrich_call.kwargs["timeout"] == 10

    @patch("cli.submit.httpx.post")
    def test_submit_succeeds_when_enrich_fails(
        self, mock_post, suite_result_with_bundles, capsys
    ):
        """Enrich errors warn to stderr but do not fail submit."""
        mock_post.side_effect = [
            MagicMock(
                status_code=201,
                json=lambda: {
                    "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                    "share_url": "https://janus-labs.dev/result/abc12345",
                    "percentile": 15.2,
                    "rank": 5,
                },
            ),
            MagicMock(status_code=500, text="boom"),
        ]

        result = submit_result(suite_result_with_bundles)

        captured = capsys.readouterr()
        assert result["status"] == "success"
        assert mock_post.call_count == 2
        assert "Warning: enrichment kickoff failed (500)" in captured.err

    @patch("cli.submit.httpx.post")
    def test_submit_no_judge_skips_enrich(self, mock_post, suite_result_with_bundles):
        """--no-judge skips both backend judging bundle extraction and enrich kickoff."""
        mock_post.return_value = MagicMock(
            status_code=201,
            json=lambda: {
                "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                "share_url": "https://janus-labs.dev/result/abc12345",
                "percentile": 15.2,
                "rank": 5,
            },
        )

        result = submit_result(suite_result_with_bundles, no_judge=True)

        assert result["status"] == "success"
        assert mock_post.call_count == 1
        assert mock_post.call_args.args[0].endswith("/api/submit")

    @patch("cli.submit.httpx.post")
    def test_single_behavior_submit_path_unchanged(self, mock_post, result_dir):
        """Single-behavior score results still submit through the legacy path."""
        result = {
            "behavior_id": "BHV-001-test-cheating",
            "score": 9.0,
            "suite_id": "refactor-storm",
            "suite_version": "1.0",
            "config_hash": "abcdef123456",
            "git_diff": {"patch": "diff --git"},
            "test_results": {"output": "1 passed"},
        }
        sample_result = result_dir / "single-result.json"
        sample_result.write_text(json.dumps(result))

        mock_post.return_value = MagicMock(
            status_code=201,
            json=lambda: {
                "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                "share_url": "https://janus-labs.dev/result/abc12345",
                "percentile": 15.2,
                "rank": 5,
            },
        )

        submit_result(str(sample_result))

        assert mock_post.call_count == 1
        payload = mock_post.call_args.kwargs["json"]
        assert payload["score"] == 90.0
        assert payload["behaviors"] == [{"code": "B-1.01", "score": 90.0, "grade": "A"}]
        assert "bundle" in payload
        assert payload["bundle"]["behavior_id"] == "BHV-001-test-cheating"

    def test_extract_bundle_reads_real_schema_bundle_file(self, single_result_with_real_bundle):
        """Bundle extraction maps the real sidecar schema into a rollout payload."""
        result = json.loads(Path(single_result_with_real_bundle).read_text())

        bundle = _extract_bundle(result, single_result_with_real_bundle)

        assert bundle is not None
        assert bundle["behavior_id"] == "BHV-001-test-cheating"
        rollout = bundle["rollouts"][0]
        assert rollout["prompt"] == "Refactor the parser"
        assert rollout["response"] == "Final response"
        assert rollout["repo_diff"] == "diff --git a/parser.py b/parser.py"
        assert rollout["test_output"] == "2 passed"
        assert all(rollout[field] for field in ("prompt", "response", "repo_diff", "test_output"))

    def test_extract_bundle_handles_legacy_and_malformed_fields(self, result_dir):
        """Bundle extraction falls back to empty strings or legacy fields without raising."""
        result = {
            "behavior_id": "BHV-001-test-cheating",
            "config_hash": "abcdef123456",
        }
        result_path = result_dir / "legacy-result.json"
        result_path.write_text(json.dumps(result))
        result_path.with_suffix(".bundle.json").write_text(
            json.dumps(
                {
                    "transcript": "invalid",
                    "repo_diff": "diff --git a/app.py b/app.py",
                    "test_results": None,
                    "test_output": "legacy output",
                }
            )
        )

        bundle = _extract_bundle(result, str(result_path))

        assert bundle is not None
        rollout = bundle["rollouts"][0]
        assert rollout == {
            "prompt": "",
            "response": "",
            "repo_diff": "diff --git a/app.py b/app.py",
            "test_output": "legacy output",
        }

    @patch("cli.submit.httpx.post")
    def test_rate_limit_exceeded(self, mock_post, sample_result):
        """Rate limit returns 429 error."""
        mock_post.return_value = MagicMock(
            status_code=429, json=lambda: {"detail": "Rate limit exceeded"}
        )

        with pytest.raises(RuntimeError, match="Rate limit exceeded"):
            submit_result(sample_result)

    @patch("cli.submit.httpx.post")
    def test_validation_error(self, mock_post, sample_result):
        """Invalid payload returns 400 error with signature mismatch."""
        mock_post.return_value = MagicMock(
            status_code=400, json=lambda: {"detail": "Invalid signature"}
        )

        with pytest.raises(RuntimeError, match="SUBMIT FAILED.*signature"):
            submit_result(sample_result)

    @patch("cli.submit.httpx.post")
    def test_submit_includes_provenance_when_present(self, mock_post, result_dir):
        """Result provenance passes through to the POST payload."""
        result = {
            "headline_score": 83.6,
            "grade": "A",
            "suite_id": "refactor-storm",
            "suite_version": "1.0",
            "config_fingerprint": "a1b2c3d4e5f6",
            "config_sources": ["CLAUDE.md"],
            "behavior_scores": [
                {"behavior_id": "O-1.11", "score": 92.0, "grade": "S"}
            ],
            "provenance": "outcome-only",
        }
        sample_result = result_dir / "result.json"
        sample_result.write_text(json.dumps(result))

        mock_post.return_value = MagicMock(
            status_code=201,
            json=lambda: {
                "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                "share_url": "https://janus-labs.dev/result/abc12345",
                "percentile": 15.2,
                "rank": 5,
            },
        )

        submit_result(str(sample_result))

        payload = mock_post.call_args.kwargs["json"]
        assert payload["provenance"] == "outcome-only"

    @patch("cli.submit.httpx.post")
    def test_submit_opens_browser_by_default(
        self, mock_post, sample_result, browser_open_calls
    ):
        """Successful submit opens the share URL in a browser by default."""
        share_url = "https://janus-labs.dev/result/abc12345"
        mock_post.return_value = MagicMock(
            status_code=201,
            json=lambda: {
                "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                "share_url": share_url,
                "percentile": 15.2,
                "rank": 5,
            },
        )

        submit_result(sample_result, github_handle="octocat")

        assert browser_open_calls == [share_url]

    @patch("cli.submit.httpx.post")
    def test_submit_no_open_flag(self, mock_post, sample_result, browser_open_calls):
        """Successful submit skips browser launch when no_open is enabled."""
        share_url = "https://janus-labs.dev/result/abc12345"
        mock_post.return_value = MagicMock(
            status_code=201,
            json=lambda: {
                "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                "share_url": share_url,
                "percentile": 15.2,
                "rank": 5,
            },
        )

        submit_result(sample_result, github_handle="octocat", no_open=True)

        assert browser_open_calls == []

    @pytest.mark.parametrize("no_open", [False, True])
    @patch("cli.submit.httpx.post")
    def test_submit_url_always_printed(
        self, mock_post, sample_result, browser_open_calls, capsys, no_open
    ):
        """Share URL is printed whether or not browser opening is disabled."""
        share_url = "https://janus-labs.dev/result/abc12345"
        mock_post.return_value = MagicMock(
            status_code=201,
            json=lambda: {
                "submission_id": "abc12345-6789-0def-ghij-klmnopqrstuv",
                "share_url": share_url,
                "percentile": 15.2,
                "rank": 5,
            },
        )

        submit_result(
            sample_result,
            github_handle="octocat",
            no_open=no_open,
        )

        captured = capsys.readouterr()
        assert "Share:" in captured.out
        assert share_url in captured.out
