"""Tests for vr push command."""

import json
import pytest
from pathlib import Path
from unittest.mock import patch, Mock

from typer.testing import CliRunner

from vr_cli.cli import app
from vr_cli.commands.push import (
    run_validation,
    package_agent_code,
    push,
)
from vr_cli.utils.agent_lock import load_agent_lock, save_agent_lock, AGENT_LOCK_FILE


runner = CliRunner()


class TestRunValidation:
    """Tests for run_validation function."""

    def test_validation_passes_for_valid_project(self, voicerun_project):
        """Should return True for valid project."""
        with patch("vr_cli.commands.push.console"):
            result = run_validation(voicerun_project)
        assert result is True

    def test_validation_fails_for_missing_handler(self, temp_dir):
        """Should return False when handler.py is missing."""
        voicerun_dir = temp_dir / ".voicerun"
        voicerun_dir.mkdir()
        (voicerun_dir / "agent.yaml").write_text("name: test\n")

        with patch("vr_cli.commands.push.console"):
            result = run_validation(temp_dir)
        assert result is False

    def test_validation_fails_for_missing_voicerun_dir(self, temp_dir):
        """Should return False when .voicerun/ is missing."""
        (temp_dir / "handler.py").write_text("async def handler(event): pass\n")

        with patch("vr_cli.commands.push.console"):
            result = run_validation(temp_dir)
        assert result is False


class TestLoadAgentLock:
    """Tests for load_agent_lock function."""

    def test_load_existing_lock(self, temp_dir):
        """Should load existing agent.lock file."""
        voicerun_dir = temp_dir / ".voicerun"
        voicerun_dir.mkdir()
        lock_data = {"id": "agent-123", "functionId": "func-456"}
        (voicerun_dir / AGENT_LOCK_FILE).write_text(json.dumps(lock_data))

        result = load_agent_lock(voicerun_dir)
        assert result == lock_data

    def test_load_nonexistent_lock(self, temp_dir):
        """Should return None when lock file doesn't exist."""
        voicerun_dir = temp_dir / ".voicerun"
        voicerun_dir.mkdir()

        result = load_agent_lock(voicerun_dir)
        assert result is None

    def test_load_invalid_json_lock(self, temp_dir):
        """Should return None for invalid JSON."""
        voicerun_dir = temp_dir / ".voicerun"
        voicerun_dir.mkdir()
        (voicerun_dir / AGENT_LOCK_FILE).write_text("invalid json")

        with patch("vr_cli.commands.push.print_error"):
            result = load_agent_lock(voicerun_dir)
        assert result is None


class TestSaveAgentLock:
    """Tests for save_agent_lock function."""

    def test_save_lock(self, temp_dir):
        """Should save agent.lock file."""
        voicerun_dir = temp_dir / ".voicerun"
        voicerun_dir.mkdir()
        lock_data = {"id": "agent-123", "functionId": "func-456"}

        result = save_agent_lock(voicerun_dir, lock_data)
        assert result is True

        saved_data = json.loads((voicerun_dir / AGENT_LOCK_FILE).read_text())
        assert saved_data == lock_data

    def test_save_lock_creates_formatted_json(self, temp_dir):
        """Should save formatted JSON with indentation."""
        voicerun_dir = temp_dir / ".voicerun"
        voicerun_dir.mkdir()
        lock_data = {"id": "agent-123", "functionId": "func-456"}

        save_agent_lock(voicerun_dir, lock_data)

        content = (voicerun_dir / AGENT_LOCK_FILE).read_text()
        assert "\n" in content  # Formatted JSON has newlines


class TestPackageAgentCode:
    """Tests for package_agent_code function."""

    def test_package_returns_base64(self, voicerun_project):
        """Should return base64-encoded zip content."""
        with patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.print_info"):
            mock_package.return_value = (b"fake-zip-content", "test.zip")

            result = package_agent_code(voicerun_project)

        assert result is not None
        # Verify it's valid base64
        import base64
        decoded = base64.b64decode(result)
        assert decoded == b"fake-zip-content"

    def test_package_returns_none_on_error(self, voicerun_project):
        """Should return None when packaging fails."""
        with patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_error"):
            mock_package.side_effect = Exception("Packaging failed")

            result = package_agent_code(voicerun_project)

        assert result is None


class TestPushCommand:
    """Tests for the push command."""

    def test_push_not_voicerun_project(self, temp_dir, monkeypatch):
        """Should fail when not in a voicerun project."""
        monkeypatch.chdir(temp_dir)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_error"), \
             patch("vr_cli.commands.push.print_info"):
            result = runner.invoke(app, ["push"])

        assert result.exit_code != 0

    def test_push_validation_failure(self, invalid_voicerun_project, monkeypatch):
        """Should fail when validation fails."""
        monkeypatch.chdir(invalid_voicerun_project)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_error"), \
             patch("vr_cli.commands.push.print_info"):
            result = runner.invoke(app, ["push"])

        assert result.exit_code != 0

    def test_push_new_agent(self, voicerun_project, monkeypatch):
        """Should create new agent when no lock file exists."""
        monkeypatch.chdir(voicerun_project)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_success"), \
             patch("vr_cli.commands.push.confirm") as mock_confirm, \
             patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.make_request") as mock_request:

            mock_confirm.return_value = True
            mock_package.return_value = (b"zip-content", "test.zip")
            mock_request.return_value = {
                "agentId": "new-agent-id",
                "functionId": "new-function-id"
            }

            result = runner.invoke(app, ["push"])

        assert result.exit_code == 0
        # Verify lock file was created
        lock_path = voicerun_project / ".voicerun" / AGENT_LOCK_FILE
        assert lock_path.exists()

    def test_push_update_existing_function(self, voicerun_project_with_lock, monkeypatch):
        """Should update existing function when lock file exists."""
        monkeypatch.chdir(voicerun_project_with_lock)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_success"), \
             patch("vr_cli.commands.push.confirm") as mock_confirm, \
             patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.make_request") as mock_request:

            mock_confirm.return_value = True
            mock_package.return_value = (b"zip-content", "test.zip")
            mock_request.return_value = {
                "agentId": "test-agent-id-123",
                "functionId": "test-function-id-456"
            }

            result = runner.invoke(app, ["push"])

        # Verify request included existing IDs
        mock_request.assert_called_once()
        call_kwargs = mock_request.call_args[1]
        assert call_kwargs["json"]["agentId"] == "test-agent-id-123"
        assert call_kwargs["json"]["functionId"] == "test-function-id-456"

    def test_push_new_version(self, voicerun_project_with_lock, monkeypatch):
        """Should create new function version with --new flag."""
        monkeypatch.chdir(voicerun_project_with_lock)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_success"), \
             patch("vr_cli.commands.push.confirm") as mock_confirm, \
             patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.make_request") as mock_request:

            mock_confirm.return_value = True
            mock_package.return_value = (b"zip-content", "test.zip")
            mock_request.return_value = {
                "agentId": "test-agent-id-123",
                "functionId": "new-function-id"
            }

            result = runner.invoke(app, ["push", "--new"])

        # Verify request did not include functionId
        mock_request.assert_called_once()
        call_kwargs = mock_request.call_args[1]
        assert "functionId" not in call_kwargs["json"]

    def test_push_with_name(self, voicerun_project, monkeypatch):
        """Should include name in request when --name is used."""
        monkeypatch.chdir(voicerun_project)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_success"), \
             patch("vr_cli.commands.push.confirm") as mock_confirm, \
             patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.make_request") as mock_request:

            mock_confirm.return_value = True
            mock_package.return_value = (b"zip-content", "test.zip")
            mock_request.return_value = {
                "agentId": "new-agent-id",
                "functionId": "new-function-id"
            }

            result = runner.invoke(app, ["push", "--name", "v2.0"])

        mock_request.assert_called_once()
        call_kwargs = mock_request.call_args[1]
        assert call_kwargs["json"]["name"] == "v2.0"

    def test_push_skip_confirmation(self, voicerun_project, monkeypatch):
        """Should skip confirmation with --yes flag."""
        monkeypatch.chdir(voicerun_project)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_success"), \
             patch("vr_cli.commands.push.confirm") as mock_confirm, \
             patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.make_request") as mock_request:

            mock_package.return_value = (b"zip-content", "test.zip")
            mock_request.return_value = {
                "agentId": "new-agent-id",
                "functionId": "new-function-id"
            }

            result = runner.invoke(app, ["push", "--yes"])

        # Confirm should not have been called
        mock_confirm.assert_not_called()

    def test_push_aborted_by_user(self, voicerun_project, monkeypatch):
        """Should abort when user declines confirmation."""
        monkeypatch.chdir(voicerun_project)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.confirm") as mock_confirm, \
             patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.make_request") as mock_request:

            mock_confirm.return_value = False
            mock_package.return_value = (b"zip-content", "test.zip")

            result = runner.invoke(app, ["push"])

        # Request should not have been made
        mock_request.assert_not_called()

    def test_push_api_failure(self, voicerun_project, monkeypatch):
        """Should handle API failure gracefully."""
        monkeypatch.chdir(voicerun_project)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_error"), \
             patch("vr_cli.commands.push.confirm") as mock_confirm, \
             patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.make_request") as mock_request:

            mock_confirm.return_value = True
            mock_package.return_value = (b"zip-content", "test.zip")
            mock_request.return_value = None  # API failure

            result = runner.invoke(app, ["push"])

        assert result.exit_code != 0


class TestPushCommandEdgeCases:
    """Edge case tests for push command."""

    def test_push_packaging_failure(self, voicerun_project, monkeypatch):
        """Should handle packaging failure."""
        monkeypatch.chdir(voicerun_project)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_error"), \
             patch("vr_cli.commands.push.package_function") as mock_package:

            mock_package.side_effect = Exception("Packaging failed")

            result = runner.invoke(app, ["push", "--yes"])

        assert result.exit_code != 0

    def test_push_updates_lock_file(self, voicerun_project, monkeypatch):
        """Should update lock file with new IDs from response."""
        monkeypatch.chdir(voicerun_project)

        with patch("vr_cli.commands.push.console"), \
             patch("vr_cli.commands.push.print_info"), \
             patch("vr_cli.commands.push.print_success"), \
             patch("vr_cli.commands.push.confirm") as mock_confirm, \
             patch("vr_cli.commands.push.package_function") as mock_package, \
             patch("vr_cli.commands.push.make_request") as mock_request:

            mock_confirm.return_value = True
            mock_package.return_value = (b"zip-content", "test.zip")
            mock_request.return_value = {
                "agentId": "response-agent-id",
                "functionId": "response-function-id"
            }

            runner.invoke(app, ["push"])

        # Verify lock file contains response IDs
        lock_path = voicerun_project / ".voicerun" / AGENT_LOCK_FILE
        lock_data = json.loads(lock_path.read_text())
        assert lock_data["id"] == "response-agent-id"
        assert lock_data["functionId"] == "response-function-id"
