# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
"""Unit tests for the CLI registry commands."""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

import pytest

from airbyte_ops_mcp.registry import (
    ConnectorMetadata,
    ConnectorPublishResult,
    is_valid_for_progressive_rollout,
    strip_rc_suffix,
)
from airbyte_ops_mcp.registry.compile import (
    _apply_release_candidates_to_entries,
)


@pytest.mark.unit
@pytest.mark.parametrize(
    "version,expected",
    [
        pytest.param("1.2.3-rc.1", True, id="rc_version"),
        pytest.param("1.2.3-rc.10", True, id="rc_version_double_digit"),
        pytest.param("0.0.1-rc.1", True, id="rc_version_zero"),
        pytest.param("1.2.3", True, id="stable_ga_version"),
        pytest.param("1.2.3-preview.abc123", False, id="preview_version_rejected"),
        pytest.param("1.2.3-alpha.1", True, id="alpha_version"),
        pytest.param("1.2.3-beta.1", True, id="beta_version"),
    ],
)
def test_is_valid_for_progressive_rollout(version: str, expected: bool) -> None:
    """Test version validation for progressive rollout."""
    assert is_valid_for_progressive_rollout(version) == expected


@pytest.mark.unit
@pytest.mark.parametrize(
    "version,expected",
    [
        pytest.param("1.2.3-rc.1", "1.2.3", id="rc_version"),
        pytest.param("1.2.3-rc.10", "1.2.3", id="rc_version_double_digit"),
        pytest.param("0.0.1-rc.1", "0.0.1", id="rc_version_zero"),
        pytest.param("1.2.3", "1.2.3", id="stable_version_unchanged"),
    ],
)
def test_strip_rc_suffix(version: str, expected: str) -> None:
    """Test stripping release candidate suffix from version."""
    assert strip_rc_suffix(version) == expected


@pytest.mark.unit
def test_connector_metadata_model() -> None:
    """Test ConnectorMetadata Pydantic model."""
    metadata = ConnectorMetadata(
        name="source-github",
        docker_repository="airbyte/source-github",
        docker_image_tag="1.2.3-rc.1",
        support_level="certified",
        definition_id="abc123",
    )
    assert metadata.name == "source-github"
    assert metadata.docker_repository == "airbyte/source-github"
    assert metadata.docker_image_tag == "1.2.3-rc.1"
    assert metadata.support_level == "certified"
    assert metadata.definition_id == "abc123"


@pytest.mark.unit
def test_connector_publish_result_model() -> None:
    """Test ConnectorPublishResult Pydantic model."""
    result = ConnectorPublishResult(
        connector="source-github",
        version="1.2.3",
        action="progressive-rollout-cleanup",
        status="success",
        docker_image="airbyte/source-github:1.2.3",
        registry_updated=True,
        message="Cleaned up release candidate",
    )
    assert result.connector == "source-github"
    assert result.version == "1.2.3"
    assert result.action == "progressive-rollout-cleanup"
    assert result.status == "success"
    assert result.docker_image == "airbyte/source-github:1.2.3"
    assert result.registry_updated is True
    assert result.message == "Cleaned up release candidate"


def run_cli(
    *args: str, cwd: str | Path | None = None
) -> subprocess.CompletedProcess[str]:
    """Run the airbyte-ops CLI with the given arguments."""
    cmd = [sys.executable, "-m", "airbyte_ops_mcp.cli.app", *args]
    return subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        cwd=cwd,
        env={**os.environ, "PYTHONPATH": str(Path(__file__).parent.parent / "src")},
    )


@pytest.mark.unit
def test_cli_help() -> None:
    """Test CLI help output."""
    result = run_cli("--help")
    assert result.returncode == 0
    assert "airbyte-ops" in result.stdout.lower()
    assert "registry" in result.stdout


@pytest.mark.unit
def test_registry_connector_version_next_help() -> None:
    """Test connector-version next help output."""
    result = run_cli("registry", "connector-version", "next", "--help")
    assert result.returncode == 0
    assert "name" in result.stdout
    assert "sha" in result.stdout
    assert "base-version" in result.stdout


@pytest.mark.unit
def test_registry_connector_version_next_missing_name() -> None:
    """Test connector-version next fails without name."""
    result = run_cli(
        "registry",
        "connector-version",
        "next",
        "--sha",
        "abcdef1",
    )
    assert result.returncode != 0


@pytest.mark.unit
def test_registry_help() -> None:
    """Test registry subcommand help output."""
    result = run_cli("registry", "--help")
    assert result.returncode == 0
    # Check for command groups
    assert "progressive-rollout" in result.stdout
    assert "connector" in result.stdout
    assert "connector-version" in result.stdout
    assert "mirror" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_help() -> None:
    """Test registry progressive-rollout sub-app help output."""
    result = run_cli("registry", "progressive-rollout", "--help")
    assert result.returncode == 0
    assert "create" in result.stdout
    assert "cleanup" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_cleanup_help() -> None:
    """Test registry progressive-rollout cleanup help output."""
    result = run_cli("registry", "progressive-rollout", "cleanup", "--help")
    assert result.returncode == 0
    assert "name" in result.stdout.lower()
    assert "dry-run" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_create_help() -> None:
    """Test registry progressive-rollout create help output."""
    result = run_cli("registry", "progressive-rollout", "create", "--help")
    assert result.returncode == 0
    assert "name" in result.stdout.lower()
    assert "dry-run" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_cleanup_missing_required_options() -> None:
    """Test that missing required options causes an error."""
    result = run_cli("registry", "progressive-rollout", "cleanup")
    assert result.returncode != 0


@pytest.mark.unit
def test_registry_progressive_rollout_create_missing_required_options() -> None:
    """Test that missing required options causes an error."""
    result = run_cli("registry", "progressive-rollout", "create")
    assert result.returncode != 0


@pytest.mark.unit
def test_registry_progressive_rollout_cleanup_dry_run(tmp_path: Path) -> None:
    """Test progressive-rollout cleanup with dry-run."""
    connector_dir = tmp_path / "airbyte-integrations" / "connectors" / "source-test"
    connector_dir.mkdir(parents=True)
    (connector_dir / "metadata.yaml").write_text(
        "data:\n  dockerRepository: airbyte/source-test\n  dockerImageTag: 1.0.0-rc.1\n"
    )
    result = run_cli(
        "registry",
        "progressive-rollout",
        "cleanup",
        "--store",
        "coral:dev",
        "--name",
        "source-test",
        "--repo-path",
        str(tmp_path),
        "--dry-run",
        cwd=tmp_path,
    )
    assert result.returncode == 0
    assert "dry-run" in result.stdout
    assert "source-test" in result.stdout
    assert "1.0.0" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_create_dry_run(tmp_path: Path) -> None:
    """Test progressive-rollout create with dry-run."""
    connector_dir = tmp_path / "airbyte-integrations" / "connectors" / "source-test"
    connector_dir.mkdir(parents=True)
    (connector_dir / "metadata.yaml").write_text(
        "data:\n  dockerRepository: airbyte/source-test\n  dockerImageTag: 1.0.0-rc.1\n"
    )
    result = run_cli(
        "registry",
        "progressive-rollout",
        "create",
        "--store",
        "coral:dev",
        "--name",
        "source-test",
        "--repo-path",
        str(tmp_path),
        "--dry-run",
        cwd=tmp_path,
    )
    assert result.returncode == 0
    assert "dry-run" in result.stdout
    assert "source-test" in result.stdout
    assert "1.0.0" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_cleanup_non_rc_version(
    tmp_path: Path,
) -> None:
    """Test progressive-rollout cleanup works even for non-RC on-disk version.

    In the promote workflow the on-disk metadata.yaml is bumped to GA
    before cleanup runs, so cleanup must NOT gate on the version format.
    """
    connector_dir = tmp_path / "airbyte-integrations" / "connectors" / "source-test"
    connector_dir.mkdir(parents=True)
    (connector_dir / "metadata.yaml").write_text(
        "data:\n  dockerRepository: airbyte/source-test\n  dockerImageTag: 1.0.0\n"
    )
    result = run_cli(
        "registry",
        "progressive-rollout",
        "cleanup",
        "--store",
        "coral:dev",
        "--name",
        "source-test",
        "--repo-path",
        str(tmp_path),
        "--dry-run",
        cwd=tmp_path,
    )
    # Should succeed even though on-disk version is GA — cleanup does not
    # gate on version format because the promote workflow bumps
    # the version before calling cleanup.
    assert result.returncode == 0
    assert "dry-run" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_create_ga_version(
    tmp_path: Path,
) -> None:
    """Test progressive-rollout create succeeds for GA version.

    GA versions are now valid for progressive rollout — no `-rc` suffix
    or `enableProgressiveRollout` metadata flag is required at this layer.
    """
    connector_dir = tmp_path / "airbyte-integrations" / "connectors" / "source-test"
    connector_dir.mkdir(parents=True)
    (connector_dir / "metadata.yaml").write_text(
        "data:\n  dockerRepository: airbyte/source-test\n  dockerImageTag: 1.0.0\n"
    )
    result = run_cli(
        "registry",
        "progressive-rollout",
        "create",
        "--store",
        "coral:dev",
        "--name",
        "source-test",
        "--repo-path",
        str(tmp_path),
        "--dry-run",
        cwd=tmp_path,
    )
    assert result.returncode == 0
    assert "dry-run" in result.stdout
    assert "source-test" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_create_preview_version_rejected(
    tmp_path: Path,
) -> None:
    """Test progressive-rollout create rejects preview versions."""
    connector_dir = tmp_path / "airbyte-integrations" / "connectors" / "source-test"
    connector_dir.mkdir(parents=True)
    (connector_dir / "metadata.yaml").write_text(
        "data:\n  dockerRepository: airbyte/source-test\n  dockerImageTag: 1.0.0-preview.abc123\n"
    )
    result = run_cli(
        "registry",
        "progressive-rollout",
        "create",
        "--store",
        "coral:dev",
        "--name",
        "source-test",
        "--repo-path",
        str(tmp_path),
        cwd=tmp_path,
    )
    assert result.returncode == 1
    assert "failure" in result.stdout
    assert "not valid for progressive rollout" in result.stdout


@pytest.mark.unit
def test_registry_progressive_rollout_cleanup_connector_not_found(
    tmp_path: Path,
) -> None:
    """Test error when connector directory doesn't exist."""
    connectors_dir = tmp_path / "airbyte-integrations" / "connectors"
    connectors_dir.mkdir(parents=True)
    result = run_cli(
        "registry",
        "progressive-rollout",
        "cleanup",
        "--store",
        "coral:dev",
        "--name",
        "source-nonexistent",
        "--repo-path",
        str(tmp_path),
        cwd=tmp_path,
    )
    assert result.returncode != 0
    assert "not found" in result.stdout.lower() or "not found" in result.stderr.lower()


@pytest.mark.unit
def test_dockerhub_help() -> None:
    """Test dockerhub subcommand help output."""
    result = run_cli("dockerhub", "--help")
    assert result.returncode == 0
    assert "inspect-image" in result.stdout


@pytest.mark.unit
def test_dockerhub_inspect_image_help() -> None:
    """Test dockerhub inspect-image help output."""
    result = run_cli("dockerhub", "inspect-image", "--help")
    assert result.returncode == 0
    assert "image" in result.stdout.lower()
    assert "tag" in result.stdout.lower()


@pytest.mark.unit
def test_registry_artifacts_generate_help() -> None:
    """Test registry connector-version artifacts generate help output."""
    result = run_cli("registry", "connector-version", "artifacts", "generate", "--help")
    assert result.returncode == 0
    assert "generate" in result.stdout.lower()


@pytest.mark.unit
def test_registry_artifacts_publish_help() -> None:
    """Test registry connector-version artifacts publish help output."""
    result = run_cli("registry", "connector-version", "artifacts", "publish", "--help")
    assert result.returncode == 0
    assert "publish" in result.stdout.lower()


@pytest.mark.unit
def test_registry_connector_version_yank_help() -> None:
    """Test registry connector-version yank help output."""
    result = run_cli("registry", "connector-version", "yank", "--help")
    assert result.returncode == 0
    assert "yank" in result.stdout.lower()


@pytest.mark.unit
def test_registry_connector_version_unyank_help() -> None:
    """Test registry connector-version unyank help output."""
    result = run_cli("registry", "connector-version", "unyank", "--help")
    assert result.returncode == 0
    assert "unyank" in result.stdout.lower()


@pytest.mark.unit
def test_registry_store_mirror_help() -> None:
    """Test registry store mirror help output."""
    result = run_cli("registry", "store", "mirror", "--help")
    assert result.returncode == 0
    assert "mirror" in result.stdout.lower()


@pytest.mark.unit
def test_registry_store_compile_help() -> None:
    """Test registry store compile help output."""
    result = run_cli("registry", "store", "compile", "--help")
    assert result.returncode == 0
    assert "compile" in result.stdout.lower()


@pytest.mark.unit
def test_registry_store_delete_dev_latest_help() -> None:
    """Test registry store delete-dev-latest help output."""
    result = run_cli("registry", "store", "delete-dev-latest", "--help")
    assert result.returncode == 0
    assert "delete" in result.stdout.lower() or "latest" in result.stdout.lower()


@pytest.mark.unit
def test_registry_connector_list_help() -> None:
    """Test registry connector list help output."""
    result = run_cli("registry", "connector", "list", "--help")
    assert result.returncode == 0
    assert "list" in result.stdout.lower()


@pytest.mark.unit
def test_registry_connector_version_list_help() -> None:
    """Test registry connector-version list help output."""
    result = run_cli("registry", "connector-version", "list", "--help")
    assert result.returncode == 0
    assert "list" in result.stdout.lower()


@pytest.mark.unit
def test_registry_connector_version_metadata_get_help() -> None:
    """Test registry connector-version metadata get help output."""
    result = run_cli("registry", "connector-version", "metadata", "get", "--help")
    assert result.returncode == 0
    assert "metadata" in result.stdout.lower() or "get" in result.stdout.lower()


@pytest.mark.unit
def test_version_flag() -> None:
    """Test that 'airbyte-ops --version' prints a version string."""
    result = run_cli("--version")
    assert result.returncode == 0
    assert result.stdout.strip(), "--version flag should produce output"


@pytest.mark.unit
def test_version_flag_not_intercepted_by_publish() -> None:
    """Regression test: --version as a subcommand param must not be hijacked.

    Previously, cyclopts' default --version meta-command on every App instance
    intercepted --version tokens meant for subcommands like 'artifacts publish
    --version 1.2.3', printing the cyclopts version and exiting 0 without
    running the actual command.
    """
    result = run_cli(
        "registry",
        "connector-version",
        "artifacts",
        "publish",
        "--name",
        "source-test",
        "--version",
        "1.2.3",
        "--artifacts-dir",
        "/nonexistent",
        "--store",
        "coral:dev",
    )
    # The command should NOT silently succeed with just a version number.
    # It should either fail (missing artifacts dir) or produce publish output.
    # The key assertion: stdout must NOT be just a bare version number.
    output = result.stdout.strip()
    assert not output.replace(".", "").isdigit(), (
        f"--version was intercepted by cyclopts meta-command: got '{output}'"
    )


@pytest.mark.unit
def test_version_flag_not_intercepted_by_yank() -> None:
    """Regression test: --version on yank must not be hijacked by cyclopts."""
    result = run_cli(
        "registry",
        "connector-version",
        "yank",
        "--name",
        "source-test",
        "--version",
        "1.2.3",
    )
    output = result.stdout.strip()
    assert not output.replace(".", "").isdigit(), (
        f"--version was intercepted by cyclopts meta-command: got '{output}'"
    )


# =============================================================================
# Compile: RC injection into global registry entries
# =============================================================================


@pytest.mark.unit
@pytest.mark.parametrize(
    "entries,rc_entries,expected_rc_key",
    [
        pytest.param(
            [
                {
                    "dockerRepository": "airbyte/source-github",
                    "dockerImageTag": "1.0.0",
                    "releases": {"breakingChanges": {}},
                },
            ],
            {
                "airbyte/source-github": {
                    "version": "1.1.0-rc.1",
                    "entry": {
                        "dockerRepository": "airbyte/source-github",
                        "dockerImageTag": "1.1.0-rc.1",
                    },
                },
            },
            "1.1.0-rc.1",
            id="single_rc_injected",
        ),
        pytest.param(
            [
                {
                    "dockerRepository": "airbyte/source-github",
                    "dockerImageTag": "1.0.0",
                },
            ],
            {},
            None,
            id="no_rcs_no_change",
        ),
        pytest.param(
            [
                {
                    "dockerRepository": "airbyte/source-github",
                    "dockerImageTag": "1.0.0",
                },
            ],
            {
                "airbyte/source-postgres": {
                    "version": "2.0.0-rc.1",
                    "entry": {
                        "dockerRepository": "airbyte/source-postgres",
                        "dockerImageTag": "2.0.0-rc.1",
                    },
                },
            },
            None,
            id="rc_for_different_connector",
        ),
    ],
)
def test_apply_release_candidates_to_entries(
    entries: list[dict],
    rc_entries: dict,
    expected_rc_key: str | None,
) -> None:
    """Test _apply_release_candidates_to_entries injects RC info correctly."""
    result = _apply_release_candidates_to_entries(entries, rc_entries)
    assert len(result) == len(entries)
    entry = result[0]
    if expected_rc_key:
        assert "releases" in entry
        assert "releaseCandidates" in entry["releases"]
        assert expected_rc_key in entry["releases"]["releaseCandidates"]
    else:
        rc_section = entry.get("releases", {}).get("releaseCandidates")
        assert rc_section is None or len(rc_section) == 0
