# Tools for A Feminist Audit of Pop Music.
# Copyright 2026 imacat.  All rights reserved.
# Authors:
#   imacat@mail.imacat.idv.tw (imacat), 2026/7/31
# AI assistance: Claude Code (Anthropic)
"""Unit tests for the package-level CLI dispatcher."""
import argparse
import io
import sys
import unittest
from contextlib import redirect_stderr, redirect_stdout
from unittest import mock

from pop_fem_audit_tools import __main__
from pop_fem_audit_tools.commands import run_llm


class TestDispatcher(unittest.TestCase):
    """Test cases for the package-level CLI dispatcher."""

    @staticmethod
    def __run_main(argv: list[str]) -> tuple[int, str, str]:
        """Run the dispatcher with captured output.

        :param argv: The command-line arguments.
        :return: A tuple of the exit status, the standard output,
            and the standard error.
        """
        stdout: io.StringIO = io.StringIO()
        stderr: io.StringIO = io.StringIO()
        with redirect_stdout(stdout), redirect_stderr(stderr):
            status: int = __main__.main(argv)
        return status, stdout.getvalue(), stderr.getvalue()

    def test_run_llm_registered(self) -> None:
        """Test that run-llm is bound to the run_llm tool main."""
        self.assertIs(__main__.SUBCOMMANDS["run-llm"], run_llm.main)

    def test_run_llm_dispatch(self) -> None:
        """Test that run-llm forwards the arguments and the status."""
        tool: mock.Mock = mock.Mock(return_value=7)
        with mock.patch.dict(__main__.SUBCOMMANDS,
                             {"run-llm": tool}):
            status: int = self.__run_main(
                ["run-llm", "--input", "items.jsonl"])[0]
        self.assertEqual(status, 7)
        tool.assert_called_once_with(["--input", "items.jsonl"])

    def test_help_exits_zero_and_lists_subcommands(self) -> None:
        """Test that --help lists the subcommands and exits 0."""
        status: int
        stdout: str
        stderr: str
        status, stdout, stderr = self.__run_main(["--help"])
        self.assertEqual(status, 0)
        self.assertIn("run-llm", stdout)

    def test_no_arguments_exits_non_zero(self) -> None:
        """Test that no arguments yields the usage and non-zero."""
        status: int
        stdout: str
        stderr: str
        status, stdout, stderr = self.__run_main([])
        self.assertNotEqual(status, 0)
        self.assertIn("run-llm", stderr)

    def test_usage_prog_module_run(self) -> None:
        """Test the usage program name when run with python -m."""
        argv: list[str] = ["/x/pop_fem_audit_tools/__main__.py"]
        stdout: str
        with mock.patch.object(sys, "argv", argv):
            stdout = self.__run_main(["--help"])[1]
        self.assertIn(
            "usage: python -m pop_fem_audit_tools ", stdout)

    def test_usage_prog_console_script(self) -> None:
        """Test the usage program name when run as a script."""
        argv: list[str] = ["/x/bin/pop-fem-audit-tools"]
        stdout: str
        with mock.patch.object(sys, "argv", argv):
            stdout = self.__run_main(["--help"])[1]
        self.assertIn("usage: pop-fem-audit-tools ", stdout)

    def test_subcommand_prog_module_run(self) -> None:
        """Test the subcommand program name with python -m."""
        argv: list[str] = ["/x/pop_fem_audit_tools/__main__.py"]
        self.assertEqual(
            self.__dispatched_prog(argv),
            "python -m pop_fem_audit_tools run-llm")

    def test_subcommand_prog_console_script(self) -> None:
        """Test the subcommand program name as a script."""
        argv: list[str] = ["/x/bin/pop-fem-audit-tools"]
        self.assertEqual(
            self.__dispatched_prog(argv),
            "pop-fem-audit-tools run-llm")

    def __dispatched_prog(self, argv: list[str]) -> str:
        """Run a stub run-llm tool and return the prog it sees.

        :param argv: The value to patch ``sys.argv`` with.
        :return: The default argparse program name seen by the
            dispatched tool.
        """
        seen: list[str] = []

        def tool(tool_argv: list[str] | None) -> int:
            """Record the argparse program name and succeed.

            :param tool_argv: The tool command-line arguments.
            :return: Always 0.
            """
            parser: argparse.ArgumentParser = \
                argparse.ArgumentParser()
            seen.append(parser.prog)
            return 0

        with mock.patch.object(sys, "argv", argv), \
                mock.patch.dict(__main__.SUBCOMMANDS,
                                {"run-llm": tool}):
            self.__run_main(["run-llm"])
        return seen[0]

    def test_unknown_subcommand_exits_non_zero(self) -> None:
        """Test that an unknown subcommand errors to standard error."""
        status: int
        stdout: str
        stderr: str
        status, stdout, stderr = self.__run_main(["nonsense"])
        self.assertNotEqual(status, 0)
        self.assertIn("nonsense", stderr)
