"""Tests for the SFTP connector.

Tests cover:
- SFTPConnectorConfig validation (correct/incorrect inputs)
- SFTPConnector construction with config and direct arguments
- SFTP operations with mocked paramiko (list, download, upload, delete)
- Reconnection logic
"""

from unittest.mock import MagicMock, patch, mock_open, PropertyMock

import pytest
import pytest_asyncio

from entr_adapter_core.connectors.sftp import SFTPConnector, SFTPConnectorConfig


# ── SFTPConnectorConfig tests ───────────────────────────────────────────


class TestSFTPConnectorConfig:
    """Validate the Pydantic config model for the SFTP connector."""

    def test_valid_config(self):
        config = SFTPConnectorConfig(
            host="sftp.example.com",
            username="user",
            password="secret",
        )
        assert config.host == "sftp.example.com"
        assert config.port == 22
        assert config.username == "user"
        assert config.password == "secret"

    def test_defaults(self):
        config = SFTPConnectorConfig(host="sftp.example.com", password="secret")
        assert config.port == 22
        assert config.username == ""

    def test_custom_port(self):
        config = SFTPConnectorConfig(host="sftp.example.com", password="secret", port=2222)
        assert config.port == 2222

    def test_port_validation_upper_bound(self):
        with pytest.raises(Exception):
            SFTPConnectorConfig(host="sftp.example.com", password="secret", port=70000)

    def test_port_validation_lower_bound(self):
        with pytest.raises(Exception):
            SFTPConnectorConfig(host="sftp.example.com", password="secret", port=0)

    def test_missing_host(self):
        with pytest.raises(Exception):
            SFTPConnectorConfig(username="user", password="secret")

    def test_missing_password(self):
        with pytest.raises(Exception):
            SFTPConnectorConfig(host="sftp.example.com", username="user")


# ── SFTPConnector construction tests ────────────────────────────────────


class TestSFTPConnectorConstruction:
    """Test SFTPConnector can be created with config or direct args."""

    def test_with_config(self):
        config = SFTPConnectorConfig(host="sftp.example.com", username="user", password="secret")
        connector = SFTPConnector(config)
        assert connector.host == "sftp.example.com"
        assert connector.port == 22
        assert connector.username == "user"

    def test_with_host_string(self):
        connector = SFTPConnector("sftp.example.com", username="user", password="secret")
        assert connector.host == "sftp.example.com"

    def test_with_keyword_host(self):
        connector = SFTPConnector(host="sftp.example.com", password="secret")
        assert connector.host == "sftp.example.com"

    def test_missing_host_raises(self):
        with pytest.raises(ValueError, match="host is required"):
            SFTPConnector()

    def test_empty_host_raises(self):
        with pytest.raises(ValueError, match="host is required"):
            SFTPConnector("", password="secret")

    def test_missing_password_raises(self):
        with pytest.raises(ValueError, match="password is required"):
            SFTPConnector("sftp.example.com")


# ── SFTP operations with mocked paramiko ────────────────────────────────


class TestSFTPOperations:
    """Test SFTP operations with mocked paramiko."""

    def _make_connector(self):
        connector = SFTPConnector("sftp.example.com", username="user", password="secret")
        # Pre-set mock connection state so _ensure_connection is a no-op
        mock_transport = MagicMock()
        mock_transport.is_active.return_value = True
        mock_sftp = MagicMock()
        connector._transport = mock_transport
        connector._sftp = mock_sftp
        return connector, mock_sftp

    @pytest.mark.asyncio
    async def test_list_files(self):
        connector, mock_sftp = self._make_connector()
        mock_sftp.listdir.return_value = ["file1.csv", "file2.csv", "readme.txt"]

        files = await connector.list_files("/data")

        assert len(files) == 3
        assert files[0]["name"] == "file1.csv"

    @pytest.mark.asyncio
    async def test_list_files_with_extension_filter(self):
        connector, mock_sftp = self._make_connector()
        mock_sftp.listdir.return_value = ["file1.csv", "file2.csv", "readme.txt"]

        files = await connector.list_files("/data", extension_filter=".csv")

        assert len(files) == 2
        assert all(f["name"].endswith(".csv") for f in files)

    @pytest.mark.asyncio
    async def test_download_file(self):
        connector, mock_sftp = self._make_connector()

        with patch("os.makedirs"):
            result = await connector.download_file("/data/file.csv", "/tmp/file.csv")

        assert result is True
        mock_sftp.get.assert_called_once_with("/data/file.csv", "/tmp/file.csv")

    @pytest.mark.asyncio
    async def test_upload_content(self):
        connector, mock_sftp = self._make_connector()

        result = await connector.upload_content("csv,data", "/outbox", "export.csv")

        assert result["status"] == "uploaded"
        assert result["filename"] == "export.csv"
        mock_sftp.putfo.assert_called_once()

    @pytest.mark.asyncio
    async def test_delete_file(self):
        connector, mock_sftp = self._make_connector()

        result = await connector.delete_file("/data/old.csv")

        assert result is True
        mock_sftp.remove.assert_called_once_with("/data/old.csv")

    @pytest.mark.asyncio
    async def test_delete_nonexistent_file(self):
        connector, mock_sftp = self._make_connector()
        mock_sftp.remove.side_effect = FileNotFoundError("No such file")

        result = await connector.delete_file("/data/gone.csv")

        # Should return True for already-deleted files
        assert result is True

    def test_close(self):
        connector, mock_sftp = self._make_connector()
        mock_transport = connector._transport

        connector.close()

        mock_sftp.close.assert_called_once()
        mock_transport.close.assert_called_once()
        assert connector._sftp is None
        assert connector._transport is None

    def test_is_connected_true(self):
        connector, _ = self._make_connector()
        assert connector._is_connected() is True

    def test_is_connected_false_no_transport(self):
        connector = SFTPConnector("sftp.example.com", password="secret")
        assert connector._is_connected() is False
