"""Unit tests for the NFO CLI module.

Tests the CLI entry point, command dispatch, and individual command functions
from src/cli/nfo_cli.py.
"""

import asyncio
from io import StringIO
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.cli.nfo_cli import (
    check_nfo_status,
    main,
    scan_and_create_nfo,
    update_nfo_files,
)

# ---------------------------------------------------------------------------
# main() dispatcher tests
# ---------------------------------------------------------------------------

class TestMainDispatcher:
    """Tests for the main() CLI entry point."""

    @patch("src.cli.nfo_cli.sys")
    def test_no_args_shows_usage(self, mock_sys, capsys):
        """No arguments prints usage text and returns 1."""
        mock_sys.argv = ["nfo_cli"]
        result = main()
        assert result == 1

    @patch("src.cli.nfo_cli.asyncio")
    @patch("src.cli.nfo_cli.sys")
    def test_scan_command_dispatches(self, mock_sys, mock_asyncio):
        """'scan' command runs scan_and_create_nfo."""
        mock_sys.argv = ["nfo_cli", "scan"]
        mock_asyncio.run.return_value = 0
        result = main()
        mock_asyncio.run.assert_called_once()

    @patch("src.cli.nfo_cli.asyncio")
    @patch("src.cli.nfo_cli.sys")
    def test_status_command_dispatches(self, mock_sys, mock_asyncio):
        """'status' command runs check_nfo_status."""
        mock_sys.argv = ["nfo_cli", "status"]
        mock_asyncio.run.return_value = 0
        result = main()
        mock_asyncio.run.assert_called_once()

    @patch("src.cli.nfo_cli.asyncio")
    @patch("src.cli.nfo_cli.sys")
    def test_update_command_dispatches(self, mock_sys, mock_asyncio):
        """'update' command runs update_nfo_files."""
        mock_sys.argv = ["nfo_cli", "update"]
        mock_asyncio.run.return_value = 0
        result = main()
        mock_asyncio.run.assert_called_once()

    @patch("src.cli.nfo_cli.sys")
    def test_unknown_command_returns_1(self, mock_sys):
        """Unknown command returns exit code 1."""
        mock_sys.argv = ["nfo_cli", "bogus"]
        result = main()
        assert result == 1

    @patch("src.cli.nfo_cli.asyncio")
    @patch("src.cli.nfo_cli.sys")
    def test_command_is_case_insensitive(self, mock_sys, mock_asyncio):
        """Command matching is case-insensitive."""
        mock_sys.argv = ["nfo_cli", "SCAN"]
        mock_asyncio.run.return_value = 0
        main()
        mock_asyncio.run.assert_called_once()


# ---------------------------------------------------------------------------
# scan_and_create_nfo tests
# ---------------------------------------------------------------------------

class TestScanAndCreateNfo:
    """Tests for scan_and_create_nfo command."""

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_1_without_tmdb_key(self, mock_settings):
        """Returns 1 when TMDB_API_KEY is missing."""
        mock_settings.tmdb_api_key = None
        result = await scan_and_create_nfo()
        assert result == 1

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_1_without_anime_directory(self, mock_settings):
        """Returns 1 when ANIME_DIRECTORY is missing."""
        mock_settings.tmdb_api_key = "key"
        mock_settings.anime_directory = None
        result = await scan_and_create_nfo()
        assert result == 1

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.SeriesManagerService")
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_0_when_no_series_found(self, mock_settings, mock_sms):
        """Returns 0 when directory has no series."""
        mock_settings.tmdb_api_key = "key"
        mock_settings.anime_directory = "/anime"
        mock_settings.nfo_auto_create = True
        mock_settings.nfo_update_on_scan = False
        mock_settings.nfo_download_poster = False
        mock_settings.nfo_download_logo = False
        mock_settings.nfo_download_fanart = False

        mock_manager = MagicMock()
        mock_serie_list = MagicMock()
        mock_serie_list.get_all.return_value = []
        mock_manager.get_serie_list.return_value = mock_serie_list
        mock_sms.from_settings.return_value = mock_manager

        result = await scan_and_create_nfo()
        assert result == 0

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.SeriesManagerService")
    @patch("src.cli.nfo_cli.settings")
    async def test_calls_scan_and_process_nfo(self, mock_settings, mock_sms):
        """Processing is invoked for found series."""
        mock_settings.tmdb_api_key = "key"
        mock_settings.anime_directory = "/anime"
        mock_settings.nfo_auto_create = True
        mock_settings.nfo_update_on_scan = False
        mock_settings.nfo_download_poster = False
        mock_settings.nfo_download_logo = False
        mock_settings.nfo_download_fanart = False

        mock_serie = MagicMock()
        mock_serie.has_nfo.return_value = False
        mock_serie.name = "Naruto"
        mock_serie.folder = "Naruto"
        mock_serie.has_poster.return_value = False
        mock_serie.has_logo.return_value = False
        mock_serie.has_fanart.return_value = False

        mock_serie_list = MagicMock()
        mock_serie_list.get_all.return_value = [mock_serie]
        mock_serie_list.load_series = MagicMock()

        mock_manager = MagicMock()
        mock_manager.get_serie_list.return_value = mock_serie_list
        mock_manager.scan_and_process_nfo = AsyncMock()
        mock_manager.close = AsyncMock()
        mock_sms.from_settings.return_value = mock_manager

        result = await scan_and_create_nfo()
        assert result == 0
        mock_manager.scan_and_process_nfo.assert_awaited_once()

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.SeriesManagerService")
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_1_on_exception(self, mock_settings, mock_sms):
        """Returns 1 when processing raises."""
        mock_settings.tmdb_api_key = "key"
        mock_settings.anime_directory = "/anime"
        mock_settings.nfo_auto_create = True
        mock_settings.nfo_update_on_scan = False
        mock_settings.nfo_download_poster = False
        mock_settings.nfo_download_logo = False
        mock_settings.nfo_download_fanart = False

        mock_serie = MagicMock()
        mock_serie.has_nfo.return_value = False
        mock_serie.name = "Test"
        mock_serie.folder = "Test"
        mock_serie_list = MagicMock()
        mock_serie_list.get_all.return_value = [mock_serie]

        mock_manager = MagicMock()
        mock_manager.get_serie_list.return_value = mock_serie_list
        mock_manager.scan_and_process_nfo = AsyncMock(
            side_effect=RuntimeError("fail")
        )
        mock_manager.close = AsyncMock()
        mock_sms.from_settings.return_value = mock_manager

        result = await scan_and_create_nfo()
        assert result == 1
        # close is called even on error (finally block)
        mock_manager.close.assert_awaited_once()


# ---------------------------------------------------------------------------
# check_nfo_status tests
# ---------------------------------------------------------------------------

class TestCheckNfoStatus:
    """Tests for check_nfo_status command."""

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_1_without_anime_directory(self, mock_settings):
        """Returns 1 when ANIME_DIRECTORY is missing."""
        mock_settings.anime_directory = None
        result = await check_nfo_status()
        assert result == 1

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_0_when_no_series(self, mock_settings):
        """Returns 0 when no series found."""
        mock_settings.anime_directory = "/anime"
        with patch("src.core.entities.SerieList.SerieList") as mock_sl:
            mock_list = MagicMock()
            mock_list.get_all.return_value = []
            mock_sl.return_value = mock_list
            result = await check_nfo_status()
        assert result == 0

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_reports_series_with_and_without_nfo(self, mock_settings, capsys):
        """Status report categorises series correctly."""
        mock_settings.anime_directory = "/anime"

        serie_a = MagicMock()
        serie_a.has_nfo.return_value = True
        serie_a.has_poster.return_value = True
        serie_a.has_logo.return_value = False
        serie_a.has_fanart.return_value = False
        serie_a.name = "A"
        serie_a.folder = "A"

        serie_b = MagicMock()
        serie_b.has_nfo.return_value = False
        serie_b.has_poster.return_value = False
        serie_b.has_logo.return_value = False
        serie_b.has_fanart.return_value = False
        serie_b.name = "B"
        serie_b.folder = "B"

        with patch(
            "src.core.entities.SerieList.SerieList"
        ) as mock_sl:
            mock_list = MagicMock()
            mock_list.get_all.return_value = [serie_a, serie_b]
            mock_sl.return_value = mock_list
            result = await check_nfo_status()

        assert result == 0


# ---------------------------------------------------------------------------
# update_nfo_files tests
# ---------------------------------------------------------------------------

class TestUpdateNfoFiles:
    """Tests for update_nfo_files command."""

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_1_without_tmdb_key(self, mock_settings):
        """Returns 1 when TMDB_API_KEY is missing."""
        mock_settings.tmdb_api_key = None
        result = await update_nfo_files()
        assert result == 1

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_1_without_anime_directory(self, mock_settings):
        """Returns 1 when ANIME_DIRECTORY is missing."""
        mock_settings.tmdb_api_key = "key"
        mock_settings.anime_directory = None
        result = await update_nfo_files()
        assert result == 1

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_0_when_no_nfo_series(self, mock_settings):
        """Returns 0 when no series have NFO files."""
        mock_settings.tmdb_api_key = "key"
        mock_settings.anime_directory = "/anime"
        mock_settings.nfo_download_poster = False
        mock_settings.nfo_download_logo = False
        mock_settings.nfo_download_fanart = False

        serie = MagicMock()
        serie.has_nfo.return_value = False

        with patch("src.core.entities.SerieList.SerieList") as mock_sl:
            mock_list = MagicMock()
            mock_list.get_all.return_value = [serie]
            mock_sl.return_value = mock_list
            result = await update_nfo_files()

        assert result == 0

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.asyncio")
    @patch("src.cli.nfo_cli.settings")
    async def test_updates_series_with_nfo(self, mock_settings, mock_sleeper):
        """Calls update_tvshow_nfo for each series with NFO."""
        mock_settings.tmdb_api_key = "key"
        mock_settings.anime_directory = "/anime"
        mock_settings.nfo_download_poster = True
        mock_settings.nfo_download_logo = False
        mock_settings.nfo_download_fanart = False
        mock_sleeper.sleep = AsyncMock()

        serie = MagicMock()
        serie.has_nfo.return_value = True
        serie.name = "Naruto"
        serie.folder = "Naruto"

        mock_nfo_svc = MagicMock()
        mock_nfo_svc.update_tvshow_nfo = AsyncMock()
        mock_nfo_svc.close = AsyncMock()

        with patch("src.core.entities.SerieList.SerieList") as mock_sl:
            mock_list = MagicMock()
            mock_list.get_all.return_value = [serie]
            mock_sl.return_value = mock_list
            with patch(
                "src.core.services.nfo_factory.create_nfo_service",
                return_value=mock_nfo_svc,
            ):
                result = await update_nfo_files()

        assert result == 0
        mock_nfo_svc.update_tvshow_nfo.assert_awaited_once()
        mock_nfo_svc.close.assert_awaited_once()

    @pytest.mark.asyncio
    @patch("src.cli.nfo_cli.settings")
    async def test_returns_1_on_factory_error(self, mock_settings):
        """Returns 1 when create_nfo_service raises ValueError."""
        mock_settings.tmdb_api_key = "key"
        mock_settings.anime_directory = "/anime"
        mock_settings.nfo_download_poster = False
        mock_settings.nfo_download_logo = False
        mock_settings.nfo_download_fanart = False

        serie = MagicMock()
        serie.has_nfo.return_value = True

        with patch("src.core.entities.SerieList.SerieList") as mock_sl:
            mock_list = MagicMock()
            mock_list.get_all.return_value = [serie]
            mock_sl.return_value = mock_list
            with patch(
                "src.core.services.nfo_factory.create_nfo_service",
                side_effect=ValueError("bad"),
            ):
                result = await update_nfo_files()

        assert result == 1
