"""
High-level Mercurial operations for test fixtures.

This module provides a helper class for performing hg operations against a running
Tool Shed server, useful for creating test fixtures that require direct hg push
(bypassing the upload API) to simulate realistic developer workflows.
"""

import os
import subprocess
from urllib.parse import urlparse

from galaxy.util import unicodify
from tool_shed.util import hg_util


class HgRepositoryOperations:
    """
    High-level hg operations for test fixtures.

    This class wraps lower-level hg utilities to provide a convenient interface
    for cloning, modifying, and pushing repositories during tests.
    """

    def __init__(self, shed_url: str, username: str, password: str):
        """
        Initialize with Tool Shed connection details.

        Args:
            shed_url: Base URL of the Tool Shed (e.g., "http://localhost:9009")
            username: Username for authentication
            password: Password for authentication
        """
        self.shed_url = shed_url.rstrip("/")
        self.username = username
        self.password = password

    def clone_repo(self, owner: str, name: str, dest_dir: str) -> str:
        """
        Clone a repository from the Tool Shed.

        Args:
            owner: Repository owner username
            name: Repository name
            dest_dir: Destination directory for the clone

        Returns:
            Path to the cloned repository

        Raises:
            Exception: If clone fails
        """
        url = f"{self.shed_url}/repos/{owner}/{name}"
        success, msg = hg_util.clone_repository(url, dest_dir, "tip")
        if not success:
            raise Exception(f"Clone failed: {msg}")
        return dest_dir

    def add_and_commit(self, repo_path: str, files: dict[str, str], message: str) -> None:
        """
        Add files and commit them to a local repository.

        Args:
            repo_path: Path to the local repository
            files: Dict mapping relative paths to file contents
            message: Commit message

        Raises:
            Exception: If add or commit fails
        """
        for rel_path, content in files.items():
            full_path = os.path.join(repo_path, rel_path)
            dir_path = os.path.dirname(full_path)
            if dir_path:
                os.makedirs(dir_path, exist_ok=True)
            with open(full_path, "w") as f:
                f.write(content)
            hg_util.add_changeset(repo_path, rel_path)
        hg_util.commit_changeset(repo_path, ".", self.username, message)

    def push(self, repo_path: str, owner: str, name: str) -> None:
        """
        Push local repository changes to the Tool Shed.

        This bypasses the upload API and pushes directly via hg protocol,
        which does NOT create RepositoryMetadata records. Use this to test
        scenarios where metadata needs to be reset/created.

        Args:
            repo_path: Path to the local repository
            owner: Repository owner username
            name: Repository name

        Raises:
            Exception: If push fails (except "no changes found" which is ok)
        """
        url = f"http://{self.username}:{self.password}@{self._host_port}/repos/{owner}/{name}"
        self._push_repository(repo_path, url)

    @property
    def _host_port(self) -> str:
        """Extract host:port from shed_url."""
        parsed = urlparse(self.shed_url)
        return f"{parsed.hostname}:{parsed.port}"

    @staticmethod
    def _push_repository(repo_path: str, dest_url: str) -> None:
        """
        Push repository changes to a remote destination.

        Args:
            repo_path: Path to the local repository
            dest_url: URL to push to (may include credentials)

        Raises:
            Exception: If push fails (except "no changes found" which is ok)
        """
        try:
            subprocess.check_output(
                ["hg", "push", dest_url],
                stderr=subprocess.STDOUT,
                cwd=repo_path,
            )
        except subprocess.CalledProcessError as e:
            output = unicodify(e.output)
            # returncode 1 with "no changes found" means nothing to push - that's ok
            if e.returncode == 1 and "no changes found" in output:
                return
            raise Exception(f"Error pushing repository: {output}")
