"""Tests for the FTP connector.

Tests cover:
- FTPConnectorConfig validation (correct/incorrect inputs)
- FTPConnector construction with config and direct arguments
- FTP operations with mocked ftplib (list, download, upload, delete)
"""

from unittest.mock import MagicMock, patch, mock_open

import pytest
import pytest_asyncio

from entr_adapter_core.connectors.ftp import FTPConnector, FTPConnectorConfig


# ── FTPConnectorConfig tests ────────────────────────────────────────────


class TestFTPConnectorConfig:
    """Validate the Pydantic config model for the FTP connector."""

    def test_valid_config(self):
        config = FTPConnectorConfig(
            host="ftp.example.com",
            username="user",
            password="pass",
        )
        assert config.host == "ftp.example.com"
        assert config.port == 21
        assert config.username == "user"
        assert config.password == "pass"

    def test_defaults(self):
        config = FTPConnectorConfig(host="ftp.example.com")
        assert config.port == 21
        assert config.username == "anonymous"
        assert config.password == ""

    def test_custom_port(self):
        config = FTPConnectorConfig(host="ftp.example.com", port=2121)
        assert config.port == 2121

    def test_port_validation_upper_bound(self):
        with pytest.raises(Exception):
            FTPConnectorConfig(host="ftp.example.com", port=70000)

    def test_port_validation_lower_bound(self):
        with pytest.raises(Exception):
            FTPConnectorConfig(host="ftp.example.com", port=0)

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


# ── FTPConnector construction tests ─────────────────────────────────────


class TestFTPConnectorConstruction:
    """Test FTPConnector can be created with config or direct args."""

    def test_with_config(self):
        config = FTPConnectorConfig(host="ftp.example.com", username="user", password="pass")
        connector = FTPConnector(config)
        assert connector.host == "ftp.example.com"
        assert connector.port == 21
        assert connector.username == "user"

    def test_with_host_string(self):
        connector = FTPConnector("ftp.example.com", username="user", password="pass")
        assert connector.host == "ftp.example.com"

    def test_with_keyword_host(self):
        connector = FTPConnector(host="ftp.example.com")
        assert connector.host == "ftp.example.com"
        assert connector.username == "anonymous"

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

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


# ── FTP operations with mocked ftplib ───────────────────────────────────


class TestFTPOperations:
    """Test FTP operations with mocked ftplib.FTP."""

    def _make_connector(self):
        return FTPConnector("ftp.example.com", username="user", password="pass")

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

        with patch.object(connector, "_connect", return_value=mock_ftp):
            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 = self._make_connector()
        mock_ftp = MagicMock()
        mock_ftp.nlst.return_value = ["file1.csv", "file2.csv", "readme.txt"]

        with patch.object(connector, "_connect", return_value=mock_ftp):
            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 = self._make_connector()
        mock_ftp = MagicMock()

        with patch.object(connector, "_connect", return_value=mock_ftp), \
             patch("builtins.open", mock_open()), \
             patch("os.makedirs"):
            result = await connector.download_file("/data/file.csv", "/tmp/file.csv")

        assert result is True
        mock_ftp.retrbinary.assert_called_once()

    @pytest.mark.asyncio
    async def test_download_file_failure(self):
        connector = self._make_connector()
        mock_ftp = MagicMock()
        mock_ftp.retrbinary.side_effect = Exception("Connection reset")

        with patch.object(connector, "_connect", return_value=mock_ftp), \
             patch("builtins.open", mock_open()), \
             patch("os.makedirs"):
            result = await connector.download_file("/data/file.csv", "/tmp/file.csv")

        assert result is False

    @pytest.mark.asyncio
    async def test_upload_content(self):
        connector = self._make_connector()
        mock_ftp = MagicMock()

        with patch.object(connector, "_connect", return_value=mock_ftp):
            result = await connector.upload_content("csv,data", "/outbox", "export.csv")

        assert result["status"] == "uploaded"
        assert result["filename"] == "export.csv"
        mock_ftp.storbinary.assert_called_once()

    @pytest.mark.asyncio
    async def test_delete_file(self):
        connector = self._make_connector()
        mock_ftp = MagicMock()
        mock_ftp.delete.return_value = "250 Delete operation successful"
        mock_ftp.nlst.return_value = []  # File no longer listed

        with patch.object(connector, "_connect", return_value=mock_ftp):
            result = await connector.delete_file("/data/old.csv")

        assert result is True
        mock_ftp.delete.assert_called_once_with("/data/old.csv")

    @pytest.mark.asyncio
    async def test_delete_nonexistent_file(self):
        connector = self._make_connector()
        mock_ftp = MagicMock()
        mock_ftp.delete.side_effect = Exception("550 No such file")

        with patch.object(connector, "_connect", return_value=mock_ftp):
            result = await connector.delete_file("/data/gone.csv")

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