"""SP37 follow-up #5: SftpClient.stat() method.

The SftpClient wrapper previously exposed ``write_file``,
``list_inbound``, ``list_inbound_names``, ``download_inbound``, and
``read_file`` — but NO ``stat()``. The canonical
``cyclone.submission.submit_file`` helper needs to compare the local
file size against the remote file size for idempotency; the lack of
``stat()`` forced the helper to bypass the wrapper and open paramiko
directly (see ``submission/core.py:_default_sftp_factory``).

This file adds a ``stat()`` method to the wrapper so the helper can
use ``SftpClient(block=b)`` instead of bypassing the wrapper. Tests
cover the stub implementation only (paramiko real-mode tests would
need a real SFTP server).
"""
from __future__ import annotations

from pathlib import Path

import pytest

from cyclone.clearhouse import SftpClient, SftpStat
from cyclone.providers import SftpBlock


def _stub_block(staging_dir: Path) -> SftpBlock:
    return SftpBlock(
        host="mft.example.com",
        port=22,
        username="user",
        auth={"password_keychain_account": "x"},
        paths={"inbound": "/inbound", "outbound": "/outbound"},
        staging_dir=str(staging_dir),
        stub=True,
    )


@pytest.fixture
def staging(tmp_path: Path) -> Path:
    """A staging dir pre-populated with one file. Mirrors what the
    SP9 stub layout looks like when an operator has dropped a real
    inbound file for testing."""
    inbound = tmp_path / "inbound"
    inbound.mkdir(parents=True)
    (inbound / "test.txt").write_bytes(b"hello world\n")
    return tmp_path


# ---- SftpStat dataclass ----------------------------------------------------- #


def test_sftp_stat_dataclass_exists():
    """The wrapper exposes a small SftpStat dataclass for the result.

    Mirrors the paramiko ``SFTPAttributes`` shape (at least ``size``)
    but keeps the public surface narrow so callers can use it
    without depending on paramiko.
    """
    assert SftpStat is not None
    stat = SftpStat(size=42, modified_at=None)
    assert stat.size == 42
    assert stat.modified_at is None


def test_sftp_stat_is_frozen():
    """SftpStat is a frozen dataclass so it can be hashed / used as a
    cache key by future callers without surprise mutation."""
    from dataclasses import FrozenInstanceError

    stat = SftpStat(size=1, modified_at=None)
    with pytest.raises(FrozenInstanceError):
        stat.size = 2  # type: ignore[misc]


# ---- Public method --------------------------------------------------------- #


def test_sftp_client_has_stat_method():
    """``SftpClient`` exposes a ``stat(remote_path)`` public method.

    Guards the API surface — a future contributor who renames or
    removes ``stat()`` (forcing the helper to bypass the wrapper
    again) would break this test, prompting them to update the
    helper instead.
    """
    assert hasattr(SftpClient, "stat")
    assert callable(SftpClient.stat)


def test_sftp_client_stat_returns_size_in_stub_mode(staging: Path):
    """Stub mode: ``stat(remote_path)`` returns the local staging
    file's size via ``os.stat_result.st_size``. Mirrors what a real
    paramiko ``SFTPAttributes.st_size`` would return for a remote file.
    """
    client = SftpClient(block=_stub_block(staging))
    stat = client.stat("/inbound/test.txt")
    assert stat.size == len(b"hello world\n")


def test_sftp_client_stat_returns_modified_at_in_stub_mode(staging: Path):
    """Stub mode: ``stat`` populates ``modified_at`` from the local
    file's mtime. Mirrors paramiko's behavior for parity."""
    client = SftpClient(block=_stub_block(staging))
    stat = client.stat("/inbound/test.txt")
    assert stat.modified_at is not None


def test_sftp_client_stat_raises_for_missing_file(staging: Path):
    """Stub mode: ``stat`` raises ``FileNotFoundError`` for a path
    that doesn't exist locally — matches the real paramiko behavior
    (which raises ``IOError``/``FileNotFoundError``). The helper's
    SKIPPED-outcome short-circuit depends on this exception type.
    """
    client = SftpClient(block=_stub_block(staging))
    with pytest.raises(FileNotFoundError):
        client.stat("/inbound/missing.txt")


def test_sftp_client_stat_handles_large_file(staging: Path):
    """Stub mode: ``stat`` returns the correct size for non-trivial files.

    Sanity check: the size path doesn't truncate or accidentally
    cast (e.g. to int8) on larger files.
    """
    payload = b"x" * (1024 * 1024)  # 1 MiB
    (staging / "inbound" / "big.bin").write_bytes(payload)
    client = SftpClient(block=_stub_block(staging))
    stat = client.stat("/inbound/big.bin")
    assert stat.size == 1024 * 1024


def test_sftp_client_stat_supports_idempotency_check(staging: Path):
    """The submission helper's idempotency check works against the
    wrapper now.

    Replays the SKIPPED-outcome check from
    ``cyclone.submission.submit_file`` (lines 178-182) using the
    wrapper directly. Before this follow-up, the helper had to open
    paramiko directly because the wrapper lacked ``stat()``.
    """
    payload = b"identical contents"
    (staging / "outbound").mkdir(parents=True, exist_ok=True)
    (staging / "outbound" / "claim.x12").write_bytes(payload)

    client = SftpClient(block=_stub_block(staging))
    local_size = len(payload)
    # stat() returns the size that was previously uploaded.
    remote_stat = client.stat("/outbound/claim.x12")
    assert remote_stat.size == local_size  # idempotency check passes
    # → SKIPPED outcome in the submission helper