import os
from pathlib import Path  # Import Path
from unittest.mock import AsyncMock, patch  # Import AsyncMock

import pytest
from typer.testing import CliRunner

from llm_min.main import (
    app,
    process_direct_url,
    process_package,
    process_requirements,
    write_full_text_file,
    write_min_text_file,
)


# Mock the search and crawler functions
@patch("llm_min.main.process_direct_url")
def test_cli_doc_url(mock_process_direct_url):
    """Test the CLI with --doc-url argument."""
    runner = CliRunner()
    doc_url = "http://example.com/docs"

    # Invoke the app with --doc-url
    result = runner.invoke(app, ["--doc-url", doc_url, "--gemini-api-key", "dummy_api_key"])

    # Assert the command ran successfully
    assert result.exit_code == 0

    # Assert that process_direct_url was called once
    mock_process_direct_url.assert_called_once()

    # Assert that process_direct_url was called with the correct arguments
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["doc_url"] == doc_url
    # Check other expected default/inferred arguments passed to process_direct_url
    assert "package_name" in kwargs  # Package name should be inferred
    assert kwargs["output_dir"] == Path("my_docs")  # Default output dir
    assert kwargs["max_crawl_pages"] == 200  # Default max pages
    assert kwargs["max_crawl_depth"] == 2  # Default max depth
    assert kwargs["chunk_size"] == 1_000_000  # Default chunk size
    # API key might be None or from env, depending on test setup, check presence is enough
    assert "gemini_api_key" in kwargs


@patch("llm_min.search.find_documentation_url")
@patch("llm_min.crawler.crawl_documentation")
def test_cli_basic(mock_crawl_documentation, mock_find_documentation_url):
    """Test the CLI with no arguments."""
    runner = CliRunner()
    result = runner.invoke(app)

    assert result.exit_code != 0  # Expecting error for no input
    # The following assertion fails because the error is logged before sys.exit(1),
    # but not printed to stdout/stderr captured by runner.
    # assert "Error: Please provide exactly one input source" in result.output
    mock_find_documentation_url.assert_not_called()  # Search shouldn't be called
    mock_crawl_documentation.assert_not_called()


@patch("llm_min.main.process_requirements")
@patch("llm_min.search.find_documentation_url")
@patch("llm_min.crawler.crawl_documentation")
def test_cli_requirements_file(
    mock_crawl_documentation,
    mock_find_documentation_url,
    mock_process_requirements,
    tmp_path,
):
    """Test the CLI with --requirements-file argument."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1\npackage2")

    result = runner.invoke(app, ["--requirements-file", str(req_file), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["packages"] == ["package1", "package2"]
    assert "output_dir" in kwargs
    assert "max_crawl_pages" in kwargs
    assert "max_crawl_depth" in kwargs
    assert "chunk_size" in kwargs
    assert "gemini_api_key" in kwargs
    mock_find_documentation_url.assert_not_called()  # Search happens inside process_requirements
    mock_crawl_documentation.assert_not_called()  # Crawl happens inside process_requirements


@patch("llm_min.main.process_requirements")
@patch("llm_min.search.find_documentation_url")
@patch("llm_min.crawler.crawl_documentation")
def test_cli_input_folder(
    mock_crawl_documentation,
    mock_find_documentation_url,
    mock_process_requirements,
    tmp_path,
):
    """Test the CLI with --input-folder argument."""
    runner = CliRunner()
    input_folder = tmp_path / "my_project"
    input_folder.mkdir()
    req_file = input_folder / "requirements.txt"
    req_file.write_text("package3\npackage4")

    result = runner.invoke(app, ["--input-folder", str(input_folder), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["packages"] == ["package3", "package4"]
    assert "output_dir" in kwargs
    assert "max_crawl_pages" in kwargs
    assert "max_crawl_depth" in kwargs
    assert "chunk_size" in kwargs
    assert "gemini_api_key" in kwargs
    mock_find_documentation_url.assert_not_called()  # Search happens inside process_requirements
    mock_crawl_documentation.assert_not_called()  # Crawl happens inside process_requirements


@patch("llm_min.main.process_requirements")
@patch("llm_min.search.find_documentation_url")
@patch("llm_min.crawler.crawl_documentation")
def test_cli_packages_string(mock_crawl_documentation, mock_find_documentation_url, mock_process_requirements):
    """Test the CLI with --packages argument."""
    runner = CliRunner()
    package_string = "package5\npackage6==1.0"

    result = runner.invoke(app, ["--packages", package_string, "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["packages"] == {"package5", "package6==1.0"}
    assert "output_dir" in kwargs
    assert "max_crawl_pages" in kwargs
    assert "max_crawl_depth" in kwargs
    assert "chunk_size" in kwargs
    assert "gemini_api_key" in kwargs
    mock_find_documentation_url.assert_not_called()
    mock_crawl_documentation.assert_not_called()


@patch("llm_min.main.process_direct_url")
@patch("llm_min.search.find_documentation_url")
@patch("llm_min.crawler.crawl_documentation")
def test_cli_doc_url_direct(mock_crawl_documentation, mock_find_documentation_url, mock_process_direct_url):
    """Test the CLI with --doc-url argument (direct processing)."""
    runner = CliRunner()
    doc_url = "http://example.com/docs/package7"

    result = runner.invoke(
        app,
        ["--doc-url", doc_url, "--gemini-api-key", "dummy_api_key"],
    )

    assert result.exit_code == 0
    mock_find_documentation_url.assert_not_called()  # Should bypass search
    mock_crawl_documentation.assert_not_called()  # Should be handled by process_direct_url
    mock_process_direct_url.assert_called_once()
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["doc_url"] == doc_url
    assert "package_name" in kwargs  # Check if package name is inferred/set
    assert "output_dir" in kwargs
    assert "max_crawl_pages" in kwargs
    assert "max_crawl_depth" in kwargs
    assert "chunk_size" in kwargs
    assert "gemini_api_key" in kwargs


def test_cli_no_input():
    """Test the CLI with no input arguments."""
    runner = CliRunner()
    result = runner.invoke(app)

    # Assert exit code is non-zero (expecting 1 from sys.exit)
    assert result.exit_code != 0
    # assert "Error: Please provide exactly one input source" in result.output # Output might be empty


def test_cli_multiple_inputs(tmp_path):
    """Test the CLI with multiple input arguments."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1")

    result = runner.invoke(app, ["--requirements-file", str(req_file), "--packages", "package2"])

    # Assert exit code is non-zero (expecting 1 from sys.exit)
    assert result.exit_code != 0
    # assert "Error: Please provide exactly one input source" in result.output # Output might be empty

    result = runner.invoke(app, ["--doc-url", "http://example.com", "--input-folder", str(tmp_path)])

    # Assert exit code is non-zero (expecting 1 from sys.exit)
    assert result.exit_code != 0
    # assert "Error: Please provide exactly one input source" in result.output # Output might be empty


# Test default values
@patch("llm_min.main.process_requirements")
def test_cli_default_args(mock_process_requirements, tmp_path):
    """Test the CLI with default argument values."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1")

    result = runner.invoke(app, ["--requirements-file", str(req_file), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["output_dir"] == Path("my_docs")
    assert kwargs["max_crawl_pages"] == 200
    assert kwargs["max_crawl_depth"] == 2
    assert kwargs["chunk_size"] == 1_000_000


@patch("llm_min.main.process_requirements")
def test_cli_max_crawl_pages_zero(mock_process_requirements, tmp_path):
    """Test the CLI with --max-crawl-pages set to 0."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1")

    result = runner.invoke(
        app,
        [
            "--requirements-file",
            str(req_file),
            "--max-crawl-pages",
            "0",
            "--gemini-api-key",
            "dummy_api_key",
        ],
    )

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["max_crawl_pages"] is None


# Test file writing functions
@patch("llm_min.main.logger")
def test_write_full_text_file(mock_logger, tmp_path):
    """Test writing full text content to a file."""
    output_dir = tmp_path / "output"
    package_name = "test_package"
    content = "This is the full text content."

    write_full_text_file(output_dir, package_name, content)

    file_path = output_dir / package_name / "llm-full.txt"
    assert file_path.is_file()
    assert file_path.read_text() == content
    mock_logger.info.assert_called_with(f"Successfully wrote full text content for {package_name} to {file_path}")


@patch("llm_min.main.logger")
def test_write_min_text_file(mock_logger, tmp_path):
    """Test writing minimal text content to a file."""
    output_dir = tmp_path / "output"
    package_name = "test_package"
    content = "This is the minimal text content."

    write_min_text_file(output_dir, package_name, content)

    file_path = output_dir / package_name / "llm-min.txt"
    assert file_path.is_file()
    assert file_path.read_text() == content
    mock_logger.info.assert_called_with(f"Successfully wrote minimal text content for {package_name} to {file_path}")


# Test process_package function
@pytest.mark.asyncio
@patch("llm_min.main.write_min_text_file")
@patch("llm_min.main.write_full_text_file")
@patch("llm_min.main.compact_content_with_llm", new_callable=AsyncMock)
@patch("llm_min.main.crawl_documentation", new_callable=AsyncMock)
@patch("llm_min.main.find_documentation_url", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_package_success(
    mock_logger,
    mock_find_documentation_url,
    mock_crawl_documentation,
    mock_compact_content_with_llm,
    mock_write_full,
    mock_write_min,
    tmp_path,
):
    """Test successful processing of a single package."""
    package_name = "test_package"
    output_dir = tmp_path / "output"
    doc_url = "http://example.com/docs/test_package"
    crawled_content = "Full documentation content."
    compacted_content = "Minimal documentation content."

    # Configure AsyncMock return values
    mock_find_documentation_url.return_value = doc_url
    mock_crawl_documentation.return_value = crawled_content
    mock_compact_content_with_llm.return_value = compacted_content

    result = await process_package(
        package_name=package_name,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert result is True
    # Use async mock assertions
    mock_find_documentation_url.assert_awaited_once_with(package_name, api_key="fake_key")
    mock_crawl_documentation.assert_awaited_once_with(doc_url, max_pages=10, max_depth=2)
    mock_write_full.assert_called_once_with(output_dir, package_name, crawled_content)
    mock_compact_content_with_llm.assert_awaited_once_with(
        aggregated_content=crawled_content,
        chunk_size=1000,
        api_key="fake_key",
        subject=package_name,
    )
    mock_write_min.assert_called_once_with(output_dir, package_name, compacted_content)
    mock_logger.info.assert_any_call(f"--- Processing package: {package_name} ---")
    mock_logger.info.assert_any_call(f"Found documentation URL for {package_name}: {doc_url}")
    mock_logger.info.assert_any_call(
        f"Successfully crawled content for {package_name}. Total size: {len(crawled_content)} characters."
    )
    mock_logger.info.assert_any_call(f"Compacting content for {package_name}...")
    mock_logger.info.assert_any_call(
        f"Successfully compacted content for {package_name}. Compacted size: {len(compacted_content)} characters."
    )
    mock_logger.info.assert_any_call(f"Finished processing package: {package_name}")


@pytest.mark.asyncio
@patch("llm_min.main.write_min_text_file")
@patch("llm_min.main.write_full_text_file")
@patch("llm_min.main.compact_content_with_llm", new_callable=AsyncMock)
@patch("llm_min.main.crawl_documentation", new_callable=AsyncMock)
@patch("llm_min.main.find_documentation_url", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_package_no_doc_url(
    mock_logger,
    mock_find_documentation_url,
    mock_crawl_documentation,
    mock_compact_content_with_llm,
    mock_write_full,
    mock_write_min,
    tmp_path,
):
    """Test processing a package when no doc URL is found."""
    package_name = "test_package"
    output_dir = tmp_path / "output"

    mock_find_documentation_url.return_value = None  # AsyncMock returns None

    result = await process_package(
        package_name=package_name,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert result is False
    mock_find_documentation_url.assert_awaited_once_with(package_name, api_key="fake_key")
    mock_crawl_documentation.assert_not_awaited()  # Use async assertion
    mock_write_full.assert_not_called()
    mock_compact_content_with_llm.assert_not_awaited()  # Use async assertion
    mock_write_min.assert_not_called()
    mock_logger.warning.assert_called_once_with(f"Could not find documentation URL for {package_name}. Skipping.")


@pytest.mark.asyncio
@patch("llm_min.main.write_min_text_file")
@patch("llm_min.main.write_full_text_file")
@patch("llm_min.main.compact_content_with_llm", new_callable=AsyncMock)
@patch("llm_min.main.crawl_documentation", new_callable=AsyncMock)
@patch("llm_min.main.find_documentation_url", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_package_no_crawled_content(
    mock_logger,
    mock_find_documentation_url,
    mock_crawl_documentation,
    mock_compact_content_with_llm,
    mock_write_full,
    mock_write_min,
    tmp_path,
):
    """Test processing a package when no content is crawled."""
    package_name = "test_package"
    output_dir = tmp_path / "output"
    doc_url = "http://example.com/docs/test_package"

    mock_find_documentation_url.return_value = doc_url
    mock_crawl_documentation.return_value = ""  # Return empty string

    result = await process_package(
        package_name=package_name,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert result is False
    mock_find_documentation_url.assert_awaited_once_with(package_name, api_key="fake_key")
    mock_crawl_documentation.assert_awaited_once_with(doc_url, max_pages=10, max_depth=2)
    mock_write_full.assert_not_called()
    mock_compact_content_with_llm.assert_not_awaited()
    mock_write_min.assert_not_called()
    mock_logger.warning.assert_called_once_with(f"No content crawled for {package_name}. Skipping.")


@pytest.mark.asyncio
@patch("llm_min.main.write_min_text_file")
@patch("llm_min.main.write_full_text_file")
@patch("llm_min.main.compact_content_with_llm", new_callable=AsyncMock)
@patch("llm_min.main.crawl_documentation", new_callable=AsyncMock)
@patch("llm_min.main.find_documentation_url", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_package_compaction_empty(
    mock_logger,
    mock_find_documentation_url,
    mock_crawl_documentation,
    mock_compact_content_with_llm,
    mock_write_full,
    mock_write_min,
    tmp_path,
):
    """Test processing a package when compaction results in empty content."""
    package_name = "test_package"
    output_dir = tmp_path / "output"
    doc_url = "http://example.com/docs/test_package"
    crawled_content = "Full documentation content."

    mock_find_documentation_url.return_value = doc_url
    mock_crawl_documentation.return_value = crawled_content
    mock_compact_content_with_llm.return_value = ""  # Return empty string

    result = await process_package(
        package_name=package_name,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert result is False  # Should return False if compaction is empty
    mock_find_documentation_url.assert_awaited_once_with(package_name, api_key="fake_key")
    mock_crawl_documentation.assert_awaited_once_with(doc_url, max_pages=10, max_depth=2)
    mock_write_full.assert_called_once_with(output_dir, package_name, crawled_content)
    mock_compact_content_with_llm.assert_awaited_once_with(
        aggregated_content=crawled_content,
        chunk_size=1000,
        api_key="fake_key",
        subject=package_name,
    )
    mock_write_min.assert_not_called()
    # Check the warning log message carefully based on the implementation
    expected_log = (
        f"Compaction failed or resulted in empty content for {package_name}. Skipping writing min file. Detail: "
    )
    mock_logger.warning.assert_called_once()
    call_args, call_kwargs = mock_logger.warning.call_args
    assert call_args[0] == expected_log  # Check the exact log message


@pytest.mark.asyncio
@patch("llm_min.main.write_min_text_file")
@patch("llm_min.main.write_full_text_file")
@patch("llm_min.main.compact_content_with_llm", new_callable=AsyncMock)
@patch("llm_min.main.crawl_documentation", new_callable=AsyncMock)
@patch("llm_min.main.find_documentation_url", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_package_exception(
    mock_logger,
    mock_find_documentation_url,
    mock_crawl_documentation,
    mock_compact_content_with_llm,
    mock_write_full,
    mock_write_min,
    tmp_path,
):
    """Test processing a package when an exception occurs."""
    package_name = "test_package"
    output_dir = tmp_path / "output"

    # Make find_documentation_url raise an exception
    mock_find_documentation_url.side_effect = Exception("Search failed")

    result = await process_package(
        package_name=package_name,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert result is False
    mock_find_documentation_url.assert_awaited_once_with(package_name, api_key="fake_key")
    mock_crawl_documentation.assert_not_awaited()
    mock_write_full.assert_not_called()
    mock_compact_content_with_llm.assert_not_awaited()
    mock_write_min.assert_not_called()
    mock_logger.error.assert_called_once()


# Test process_direct_url function
@pytest.mark.asyncio
@patch("llm_min.main.write_min_text_file")
@patch("llm_min.main.write_full_text_file")
@patch("llm_min.main.compact_content_with_llm", new_callable=AsyncMock)
@patch("llm_min.main.crawl_documentation", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_direct_url_success(
    mock_logger,
    mock_crawl_documentation,
    mock_compact_content_with_llm,
    mock_write_full,
    mock_write_min,
    tmp_path,
):
    """Test successful processing of a direct URL."""
    package_name = "crawled_doc"
    doc_url = "http://example.com/direct/docs"
    output_dir = tmp_path / "output"
    crawled_content = "Full documentation content from direct URL."
    compacted_content = "Minimal documentation content from direct URL."

    mock_crawl_documentation.return_value = crawled_content
    mock_compact_content_with_llm.return_value = compacted_content

    result = await process_direct_url(
        package_name=package_name,
        doc_url=doc_url,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert result is True
    mock_crawl_documentation.assert_awaited_once_with(doc_url, max_pages=10, max_depth=2)
    mock_write_full.assert_called_once_with(output_dir, package_name, crawled_content)
    mock_compact_content_with_llm.assert_awaited_once_with(
        aggregated_content=crawled_content,
        chunk_size=1000,
        api_key="fake_key",
        subject=package_name,
    )
    mock_write_min.assert_called_once_with(output_dir, package_name, compacted_content)
    mock_logger.info.assert_any_call(f"--- Processing direct URL for {package_name}: {doc_url} ---")
    mock_logger.info.assert_any_call(
        f"Successfully crawled content from {doc_url}. Total size: {len(crawled_content)} characters."
    )
    mock_logger.info.assert_any_call(f"Compacting content for {package_name}...")
    mock_logger.info.assert_any_call(
        f"Successfully compacted content for {package_name}. Compacted size: {len(compacted_content)} characters."
    )
    mock_logger.info.assert_any_call(f"Finished processing direct URL: {doc_url}")


@pytest.mark.asyncio
async def test_process_direct_url_exception(tmp_path):
    """Test processing a direct URL when an exception occurs during crawl."""
    package_name = "crawled_doc"
    doc_url = "http://example.com/direct/docs"
    output_dir = tmp_path / "output"

    # Patch within the function body, using AsyncMock for async functions
    with (
        patch("llm_min.main.logger") as mock_logger,
        patch("llm_min.main.crawl_documentation", new_callable=AsyncMock) as mock_crawl_documentation,
        patch("llm_min.main.compact_content_with_llm", new_callable=AsyncMock) as mock_compact_content_with_llm,
        patch("llm_min.main.write_full_text_file") as mock_write_full,
        patch("llm_min.main.write_min_text_file") as mock_write_min,
    ):
        # Make the crawl documentation mock raise an exception
        mock_crawl_documentation.side_effect = Exception("Crawl failed")

        result = await process_direct_url(
            package_name=package_name,
            doc_url=doc_url,
            output_dir=output_dir,
            max_crawl_pages=10,
            max_crawl_depth=2,
            chunk_size=1000,
            gemini_api_key="fake_key",
        )

        assert result is False  # Expecting False because exception should be caught
        mock_crawl_documentation.assert_awaited_once_with(doc_url, max_pages=10, max_depth=2)
        mock_write_full.assert_not_called()
        mock_compact_content_with_llm.assert_not_awaited()
        mock_write_min.assert_not_called()
        mock_logger.error.assert_called_once()


# Test process_requirements function
@pytest.mark.asyncio
@patch("llm_min.main.process_package", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_requirements_success(mock_logger, mock_process_package, tmp_path):
    """Test successful processing of multiple packages."""
    packages = {"package1", "package2"}
    output_dir = tmp_path / "output"

    # Configure mock process_package to return True when awaited
    mock_process_package.return_value = True

    await process_requirements(
        packages=packages,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    # Use await_count for async mock
    assert mock_process_package.await_count == len(packages)
    calls = mock_process_package.await_args_list
    called_packages = {call.args[0] for call in calls}  # Access args via call object
    assert called_packages == packages
    for call in calls:
        args = call.args  # Access args via call object
        assert args[1] == output_dir
        assert args[2] == 10  # max_crawl_pages
        assert args[3] == 2  # max_crawl_depth
        assert args[4] == 1000  # chunk_size
        assert args[5] == "fake_key"  # gemini_api_key


@pytest.mark.asyncio
@patch("llm_min.main.process_package", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_requirements_empty_list(mock_logger, mock_process_package, tmp_path):
    """Test processing an empty list of packages."""
    packages: set[str] = set()
    output_dir = tmp_path / "output"

    with pytest.raises(SystemExit) as excinfo:
        await process_requirements(
            packages=packages,
            output_dir=output_dir,
            max_crawl_pages=10,
            max_crawl_depth=2,
            chunk_size=1000,
            gemini_api_key="fake_key",
        )

    assert excinfo.value.code == 0
    mock_logger.warning.assert_called_once_with("No packages provided for processing. Exiting.")
    mock_process_package.assert_not_awaited()  # Use async assertion


@pytest.mark.asyncio
@patch("llm_min.main.process_package", new_callable=AsyncMock)
@patch("llm_min.main.logger")
async def test_process_requirements_partial_failure(mock_logger, mock_process_package, tmp_path):
    """Test processing multiple packages with partial failures."""
    packages = {"package1", "package2", "package3"}
    output_dir = tmp_path / "output"

    # Async side effect function
    async def side_effect(
        package_name,
        output_dir_arg,
        max_pages_arg,
        max_depth_arg,
        chunk_size_arg,
        api_key_arg,
    ):
        if package_name == "package2":
            return False
        return True

    mock_process_package.side_effect = side_effect

    await process_requirements(
        packages=packages,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert mock_process_package.await_count == len(packages)  # Use await_count
    called_packages = {call.args[0] for call in mock_process_package.await_args_list}
    assert called_packages == packages


# Test URL inference for --doc-url
@patch("llm_min.main.process_direct_url")
@patch.dict(os.environ, {}, clear=True)
def test_cli_doc_url_url_inference(mock_process_direct_url):
    """Test URL inference for package name with --doc-url (mocked)."""
    runner = CliRunner()

    # Test with path parts
    result = runner.invoke(
        app,
        [
            "--doc-url",
            "https://docs.python.org/3/library/os.html",
            "--gemini-api-key",
            "dummy_api_key",
        ],
    )
    assert result.exit_code == 0
    mock_process_direct_url.assert_called_with(
        package_name="3.library.os.html",
        doc_url="https://docs.python.org/3/library/os.html",
        output_dir=Path("my_docs"),
        max_crawl_pages=200,
        max_crawl_depth=2,
        chunk_size=1000000,
        gemini_api_key="dummy_api_key",
    )

    # Test with domain only
    mock_process_direct_url.reset_mock()
    result = runner.invoke(
        app,
        [
            "--doc-url",
            "https://requests.readthedocs.io/",
            "--gemini-api-key",
            "dummy_api_key",
        ],
    )
    assert result.exit_code == 0
    mock_process_direct_url.assert_called_with(
        package_name="requests",
        doc_url="https://requests.readthedocs.io/",
        output_dir=Path("my_docs"),
        max_crawl_pages=200,
        max_crawl_depth=2,
        chunk_size=1000000,
        gemini_api_key="dummy_api_key",
    )

    # Test with localhost
    mock_process_direct_url.reset_mock()
    result = runner.invoke(app, ["--doc-url", "http://localhost", "--gemini-api-key", "dummy_api_key"])
    assert result.exit_code == 0
    mock_process_direct_url.assert_called_with(
        package_name="localhost",
        doc_url="http://localhost",
        output_dir=Path("my_docs"),
        max_crawl_pages=200,
        max_crawl_depth=2,
        chunk_size=1000000,
        gemini_api_key="dummy_api_key",
    )

    # Test with complex URL
    mock_process_direct_url.reset_mock()
    result = runner.invoke(
        app,
        [
            "--doc-url",
            "https://example.com/path/to/docs/v1.2/index.html",
            "--gemini-api-key",
            "dummy_api_key",
        ],
    )
    assert result.exit_code == 0
    mock_process_direct_url.assert_called_with(
        package_name="path.to.docs.v1.2.index.html",
        doc_url="https://example.com/path/to/docs/v1.2/index.html",
        output_dir=Path("my_docs"),
        max_crawl_pages=200,
        max_crawl_depth=2,
        chunk_size=1000000,
        gemini_api_key="dummy_api_key",
    )


# Add mocks for process_direct_url to check inferred package name
@patch("llm_min.main.process_direct_url")
def test_cli_doc_url_url_inference_mocked(mock_process_direct_url):
    """Test URL inference for package name with --doc-url using mock."""
    runner = CliRunner()

    # Test with path parts
    runner.invoke(
        app,
        [
            "--doc-url",
            "https://docs.python.org/3/library/os.html",
            "--gemini-api-key",
            "dummy_api_key",
        ],
    )
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["package_name"] == "3.library.os.html"

    # Test with domain only
    mock_process_direct_url.reset_mock()
    runner.invoke(
        app,
        [
            "--doc-url",
            "https://requests.readthedocs.io/",
            "--gemini-api-key",
            "dummy_api_key",
        ],
    )
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["package_name"] == "requests"

    # Test with localhost (updated expectation)
    mock_process_direct_url.reset_mock()
    runner.invoke(app, ["--doc-url", "http://localhost", "--gemini-api-key", "dummy_api_key"])
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["package_name"] == "localhost"

    # Test with complex URL
    mock_process_direct_url.reset_mock()
    runner.invoke(
        app,
        [
            "--doc-url",
            "https://example.com/path/to/docs/v1.2/index.html",
            "--gemini-api-key",
            "dummy_api_key",
        ],
    )
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["package_name"] == "path.to.docs.v1.2.index.html"


# Test GEMINI_API_KEY environment variable
@patch.dict(os.environ, {"GEMINI_API_KEY": "env_fake_key"})
@patch("llm_min.main.process_requirements")
def test_cli_gemini_api_key_env(mock_process_requirements, tmp_path):
    """Test GEMINI_API_KEY is picked up from environment variable."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1")

    result = runner.invoke(app, ["--requirements-file", str(req_file), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["gemini_api_key"] == "dummy_api_key"


# Test --gemini-api-key command line argument overrides environment variable
@patch.dict(os.environ, {"GEMINI_API_KEY": "env_fake_key"})
@patch("llm_min.main.process_requirements")
def test_cli_gemini_api_key_override(mock_process_requirements, tmp_path):
    """Test --gemini-api-key argument overrides environment variable."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1")

    result = runner.invoke(app, ["--requirements-file", str(req_file), "--gemini-api-key", "cli_fake_key"])

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["gemini_api_key"] == "cli_fake_key"


# Test --gemini-api-key command line argument without environment variable
@patch.dict(os.environ, {}, clear=True)
@patch("llm_min.main.process_requirements")
def test_cli_gemini_api_key_cli_only(mock_process_requirements, tmp_path):
    """Test --gemini-api-key argument when environment variable is not set."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1")

    result = runner.invoke(app, ["--requirements-file", str(req_file), "--gemini-api-key", "cli_fake_key"])

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["gemini_api_key"] == "cli_fake_key"


# Test no GEMINI_API_KEY provided (neither env nor cli)
@patch.dict(os.environ, {}, clear=True)
@patch("llm_min.main.process_requirements")
def test_cli_no_gemini_api_key(mock_process_requirements, tmp_path):
    """Test no GEMINI_API_KEY is passed when neither env nor cli arg is provided."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1")

    result = runner.invoke(app, ["--requirements-file", str(req_file), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["gemini_api_key"] == "dummy_api_key"


# Test output directory creation
@patch("llm_min.main.process_requirements")
def test_cli_output_dir_exists(mock_process_requirements, tmp_path):
    """Test that the output directory is not recreated if it already exists."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("package1")
    output_dir = tmp_path / "existing_output_dir"
    output_dir.mkdir()
    assert output_dir.is_dir()

    result = runner.invoke(
        app,
        [
            "--requirements-file",
            str(req_file),
            "--output-dir",
            str(output_dir),
            "--gemini-api-key",
            "dummy_api_key",
        ],
    )

    assert result.exit_code == 0
    assert output_dir.is_dir()
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["output_dir"].resolve() == output_dir.resolve()


# Test input_folder validation (no requirements.txt)
@patch("llm_min.main.process_requirements")
def test_cli_input_folder_no_requirements_file(mock_process_requirements, tmp_path):
    """Test --input-folder validation when no requirements.txt is found."""
    runner = CliRunner()
    input_folder = tmp_path / "empty_project"
    input_folder.mkdir()

    result = runner.invoke(app, ["--input-folder", str(input_folder), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code != 0
    # assert (
    #    f"Error: Could not find requirements.txt in folder: {input_folder}"
    #    in result.output # Logged, not in CLI output
    # )
    mock_process_requirements.assert_not_called()


# Test requirements_file validation (does not exist)
@patch("llm_min.main.process_requirements")
def test_cli_requirements_file_not_exists(mock_process_requirements, tmp_path):
    """Test --requirements-file validation when the file does not exist."""
    runner = CliRunner()
    req_file = tmp_path / "non_existent_reqs.txt"
    assert not req_file.exists()

    result = runner.invoke(app, ["--requirements-file", str(req_file), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code != 0
    assert result.exit_code == 2  # Typer validation errors usually exit with 2
    # assert "does not exist" in result.output # Check for Typer's message (flaky)
    mock_process_requirements.assert_not_called()


# Test requirements_file validation (is a directory)
@patch("llm_min.main.process_requirements")
def test_cli_requirements_file_is_dir(mock_process_requirements, tmp_path):
    """Test --requirements-file validation when the path is a directory."""
    runner = CliRunner()
    req_dir = tmp_path / "a_directory"
    req_dir.mkdir()
    assert req_dir.is_dir()

    result = runner.invoke(app, ["--requirements-file", str(req_dir), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code != 0
    assert result.exit_code == 2  # Typer validation errors usually exit with 2
    assert "is a directory" in result.output  # Check for Typer's message
    # assert 'Path "a_directory" is a directory' in result.output
    mock_process_requirements.assert_not_called()


# Test input_folder validation (is a file)
@patch("llm_min.main.process_requirements")
def test_cli_input_folder_is_file(mock_process_requirements, tmp_path):
    """Test --input-folder validation when the path is a file."""
    runner = CliRunner()
    input_file = tmp_path / "a_file.txt"
    input_file.touch()
    assert input_file.is_file()

    result = runner.invoke(app, ["--input-folder", str(input_file), "--gemini-api-key", "dummy_api_key"])

    assert result.exit_code != 0
    assert result.exit_code == 2  # Typer validation errors usually exit with 2
    assert "is a file" in result.output  # Check for Typer's message
    # assert 'Path "a_file.txt" is a file' in result.output
    mock_process_requirements.assert_not_called()


# Test process_requirements with no packages (should exit)
@pytest.mark.asyncio
@patch("llm_min.main.logger")
async def test_process_requirements_no_packages(mock_logger, tmp_path):
    output_dir = tmp_path / "output"
    packages: set[str] = set()

    with pytest.raises(SystemExit) as excinfo:
        await process_requirements(
            packages=packages,
            output_dir=output_dir,
            max_crawl_pages=10,
            max_crawl_depth=2,
            chunk_size=1000,
            gemini_api_key="fake_key",
        )

    assert excinfo.value.code == 0
    mock_logger.warning.assert_called_once_with("No packages provided for processing. Exiting.")


# Test process_direct_url with URL inference edge cases
@patch("llm_min.main.process_direct_url")
def test_cli_doc_url_url_inference_edge_cases_mocked(mock_process_direct_url):
    """Test URL inference edge cases for --doc-url using mock."""
    runner = CliRunner()

    runner.invoke(app, ["--doc-url", "https://example.com/", "--gemini-api-key", "dummy_api_key"])
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["package_name"] == "example"

    mock_process_direct_url.reset_mock()
    runner.invoke(app, ["--doc-url", "https://example.com", "--gemini-api-key", "dummy_api_key"])
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["package_name"] == "example"

    mock_process_direct_url.reset_mock()
    runner.invoke(app, ["--doc-url", "/path/to/docs", "--gemini-api-key", "dummy_api_key"])
    args, kwargs = mock_process_direct_url.call_args
    assert kwargs["package_name"] == "path.to.docs"


# Test write_full_text_file exception handling
@patch("llm_min.main.logger")
@patch("builtins.open", side_effect=OSError("Disk full"))
def test_write_full_text_file_exception(mock_open, mock_logger, tmp_path):
    """Test exception handling in write_full_text_file."""
    output_dir = tmp_path / "output"
    package_name = "test_package"
    content = "Some content."

    write_full_text_file(output_dir, package_name, content)

    mock_logger.error.assert_called_once()
    args, kwargs = mock_logger.error.call_args
    assert f"Failed to write full text file for {package_name}" in args[0]
    assert "Disk full" in str(args[0])
    assert kwargs["exc_info"] is True


# Test write_min_text_file exception handling
@patch("llm_min.main.logger")
@patch("builtins.open", side_effect=OSError("Permission denied"))
def test_write_min_text_file_exception(mock_open, mock_logger, tmp_path):
    """Test exception handling in write_min_text_file."""
    output_dir = tmp_path / "output"
    package_name = "test_package"
    content = "Some content."

    write_min_text_file(output_dir, package_name, content)

    mock_logger.error.assert_called_once()
    args, kwargs = mock_logger.error.call_args
    assert f"Failed to write minimal text file for {package_name}" in args[0]
    assert "Permission denied" in str(args[0])
    assert kwargs["exc_info"] is True


# Test process_package with write_full_text_file failure
@pytest.mark.asyncio
@patch("llm_min.main.write_min_text_file")
@patch("llm_min.main.write_full_text_file", side_effect=Exception("Write full failed"))
@patch("llm_min.main.compact_content_with_llm")
@patch("llm_min.main.crawl_documentation")
@patch("llm_min.main.find_documentation_url")
@patch("llm_min.main.logger")
async def test_process_package_write_full_failure(
    mock_logger,
    mock_find_documentation_url,
    mock_crawl_documentation,
    mock_compact_content_with_llm,
    mock_write_full,
    mock_write_min,
    tmp_path,
):
    """Test process_package returns False if write_full_text_file raises exception."""
    package_name = "test_package"
    output_dir = tmp_path / "output"
    doc_url = "http://example.com/docs/test_package"
    crawled_content = "Full documentation content."

    mock_find_documentation_url.return_value = doc_url
    mock_crawl_documentation.return_value = crawled_content

    result = await process_package(
        package_name=package_name,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert result is False  # Expect False due to outer exception handling
    mock_find_documentation_url.assert_called_once_with(package_name, api_key="fake_key")
    mock_crawl_documentation.assert_called_once_with(doc_url, max_pages=10, max_depth=2)
    mock_write_full.assert_called_once_with(output_dir, package_name, crawled_content)
    mock_compact_content_with_llm.assert_not_called()  # Should not be called after write_full fails
    mock_write_min.assert_not_called()  # Should not be called
    mock_logger.error.assert_called_once()  # Outer exception handler should log


# Test process_direct_url with write_full_text_file failure
@pytest.mark.asyncio
@patch("llm_min.main.write_min_text_file")
@patch("llm_min.main.write_full_text_file", side_effect=Exception("Write full failed"))
@patch("llm_min.main.compact_content_with_llm")
@patch("llm_min.main.crawl_documentation")
@patch("llm_min.main.logger")
async def test_process_direct_url_write_full_failure(
    mock_logger,
    mock_crawl_documentation,
    mock_compact_content_with_llm,
    mock_write_full,
    mock_write_min,
    tmp_path,
):
    """Test process_direct_url returns False if write_full_text_file raises exception."""
    package_name = "crawled_doc"
    doc_url = "http://example.com/direct/docs"
    output_dir = tmp_path / "output"
    crawled_content = "Full documentation content from direct URL."

    mock_crawl_documentation.return_value = crawled_content

    result = await process_direct_url(
        package_name=package_name,
        doc_url=doc_url,
        output_dir=output_dir,
        max_crawl_pages=10,
        max_crawl_depth=2,
        chunk_size=1000,
        gemini_api_key="fake_key",
    )

    assert result is False  # Expect False due to outer exception handling
    mock_crawl_documentation.assert_called_once_with(doc_url, max_pages=10, max_depth=2)
    mock_write_full.assert_called_once_with(output_dir, package_name, crawled_content)
    mock_compact_content_with_llm.assert_not_called()  # Should not be called after write_full fails
    mock_write_min.assert_not_called()  # Should not be called
    mock_logger.error.assert_called_once()  # Outer exception handler should log


@patch.dict(os.environ, {}, clear=True)
@patch("llm_min.main.process_requirements")
def test_cli_dummy_api_key(mock_process_requirements, tmp_path):
    """Test CLI passes the literal dummy_api_key correctly."""
    runner = CliRunner()
    req_file = tmp_path / "requirements.txt"
    req_file.write_text("dummy_pkg_cli")
    dummy_key = "dummy_api_key"

    result = runner.invoke(
        app,
        [
            "--requirements-file",
            str(req_file),
            "--gemini-api-key",
            dummy_key,
        ],
    )

    assert result.exit_code == 0
    mock_process_requirements.assert_called_once()
    args, kwargs = mock_process_requirements.call_args
    assert kwargs["gemini_api_key"] == dummy_key
