import textwrap
from pathlib import Path

import pytest
from markdown_it.token import Token
import tempfile
from pathlib import Path

from alt_text_llm.scan import build_queue, _is_video_label_meaningful


from alt_text_llm import scan


@pytest.mark.parametrize(
    "alt, expected",
    [
        (None, False),
        ("", False),
        ("   ", False),
        ("image", False),
        ("A meaningful description", True),
        ("Meaningful", True),
    ],
)
def test_is_alt_meaningful(alt: str | None, expected: bool) -> None:
    assert scan._is_alt_meaningful(alt) is expected


def _write_md(tmp_path: Path, content: str, name: str = "test.md") -> Path:
    file_path = tmp_path / name
    file_path.write_text(content, encoding="utf-8")
    return file_path


def test_build_queue_markdown_asset(tmp_path: Path) -> None:
    md_content = """
Paragraph one.

![](img/foo.png)

Paragraph two.
"""
    _write_md(tmp_path, md_content)
    queue = scan.build_queue(tmp_path)
    assert len(queue) == 1
    item = queue[0]
    assert item.asset_path == "img/foo.png"
    assert item.line_number == 4
    assert "Paragraph one." in item.context_snippet
    assert "Paragraph two." in item.context_snippet


def test_build_queue_html_img_missing_alt(tmp_path: Path) -> None:
    md_content = """
Intro.

<img src=\"assets/pic.jpg\">
"""
    _write_md(tmp_path, md_content, "html.md")
    queue = scan.build_queue(tmp_path)
    assert len(queue) == 1, f"{queue} doesn't have the right elements"
    assert queue[0].asset_path == "assets/pic.jpg"


def test_build_queue_ignores_good_alt(tmp_path: Path) -> None:
    md_content = "![](foo.png)\n\n![Good alt](bar.png)"
    _write_md(tmp_path, md_content)
    queue = scan.build_queue(tmp_path)

    # only the empty alt should be queued
    assert len(queue) == 1, f"{queue} doesn't have the right elements"
    assert queue[0].asset_path == "foo.png"


@pytest.mark.parametrize(
    "content, expected_paths",
    [
        ("![](img/blank.png)", ["img/blank.png"]),
        ("![Good desc](img/good.png)", []),
        (
            '<img src="assets/foo.jpg" alt="photo">\n',
            ["assets/foo.jpg"],
        ),
        (
            '<img src="assets/bar.jpg" alt="Meaningful description">\n',
            [],
        ),
        (
            '<img src="assets/baz.jpg" alt="">\n',
            [],
        ),
    ],
)
def test_queue_expected_paths(
    tmp_path: Path, content: str, expected_paths: list[str]
) -> None:
    """Verify that *build_queue* includes exactly the expected offending assets."""

    file_path = tmp_path / "edge.md"
    file_path.write_text(content, encoding="utf-8")

    queue = scan.build_queue(tmp_path)
    assert sorted(item.asset_path for item in queue) == sorted(expected_paths)


@pytest.mark.parametrize(
    "md_content,expected_line",
    [
        pytest.param(
            "\nIntro line.\n\n<img src=\"assets/foo.jpg\">\n\nAfter image.\n",
            4,
            id="fallback_no_frontmatter",
        ),
        pytest.param(
            "\n---\ntitle: Sample\n---\n\nPreamble text.\n\n<img src=\"assets/bar.jpg\">\n",
            None,  # computed dynamically
            id="with_frontmatter",
        ),
    ],
)
def test_html_img_line_number(
    tmp_path: Path, md_content: str, expected_line: int | None
) -> None:
    """Line numbers for HTML images should be correct, even after front-matter."""
    file_path = _write_md(tmp_path, md_content, "test.md")
    if expected_line is None:
        expected_line = next(
            idx + 1
            for idx, ln in enumerate(file_path.read_text().splitlines())
            if "<img" in ln
        )
    queue = scan.build_queue(tmp_path)
    assert len(queue) == 1
    assert queue[0].line_number == expected_line


def test_get_line_number_raises_error_when_asset_not_found(
    tmp_path: Path,
) -> None:
    """Test that _get_line_number raises ValueError when asset can't be found in file."""

    # Create a markdown file without the asset we're looking for
    md_content = textwrap.dedent(
        """
        # Title
        
        Some content here.
        
        ![Different image](other.png)
        """
    )
    file_path = _write_md(tmp_path, md_content, "missing_asset.md")

    # Create a token without map info to force fallback search
    token = Token("image", "", 0)
    token.map = None

    lines = file_path.read_text().splitlines()

    # This should raise ValueError since "nonexistent.png" is not in the file
    with pytest.raises(
        ValueError,
        match="Could not find asset '\\(nonexistent.png\\)' in markdown file",
    ):
        scan._get_line_number(token, lines, "(nonexistent.png)")


def test_html_img_error_when_src_not_in_content(tmp_path: Path) -> None:
    """Test that HTML img with empty alt (decorative) is not queued."""
    md_content = textwrap.dedent(
        """
        # Title
        
        Some content.
        
        <img src="findable.jpg" alt="">
        """
    )
    _write_md(tmp_path, md_content, "html_test.md")

    # Empty alt indicates decorative image, should not be queued
    queue = scan.build_queue(tmp_path)
    assert len(queue) == 0


@pytest.mark.parametrize(
    "content, expected_paths",
    [
        # Wikilink without alt
        ("![[assets/image.png]]", ["assets/image.png"]),
        # Wikilink without meaningful alt
        ("![[assets/image.png|]]", ["assets/image.png"]),
        # Wikilink with meaningful alt
        ("![[assets/image.png|Good description]]", []),
        # Wikilink with placeholder alt
        ("![[assets/image.png|image]]", ["assets/image.png"]),
        # Wikilink with URL
        ("![[https://example.com/image.png]]", ["https://example.com/image.png"]),
        # Multiple wikilinks mixed
        (
            "![[img1.png|image]]\n![[img2.png|Good alt]]\n![[img3.png]]",
            ["img1.png", "img3.png"],
        ),
        # Multiple placeholders plus good alt
        (
            "![[assets/image.png|image]]\n![[assets/photo.jpg|photo]]\n![[assets/pic.png|Good description]]",
            ["assets/image.png", "assets/photo.jpg"],
        ),
        # URL wikilink with good alt
        (
            "![[https://example.com/image.png]]\n![[https://example.com/photo.jpg|Good alt text]]",
            ["https://example.com/image.png"],
        ),
    ],
)
def test_wikilink_formats(
    tmp_path: Path, content: str, expected_paths: list[str]
) -> None:
    """Test wikilink image detection with various formats."""
    _write_md(tmp_path, content, "wikilink.md")
    queue = scan.build_queue(tmp_path)
    assert sorted(item.asset_path for item in queue) == sorted(expected_paths)


def test_mixed_image_formats(tmp_path: Path) -> None:
    """Test that markdown, HTML, and wikilink images are all detected."""
    md_content = (
        "![](markdown.png)\n"
        '<img src="html.jpg">\n'
        "![[wikilink.gif]]\n"
        "![Good](good-md.png)\n"
        '<img src="good-html.jpg" alt="Good">\n'
        "![[good-wiki.png|Good]]"
    )
    _write_md(tmp_path, md_content, "mixed.md")

    queue = scan.build_queue(tmp_path)
    paths = {item.asset_path for item in queue}
    assert paths == {"markdown.png", "html.jpg", "wikilink.gif"}


def test_build_queue_wikilink_without_alt(tmp_path: Path) -> None:
    """Test that wikilink images without alt text are detected."""
    md_content = textwrap.dedent(
        """
        # Test Document
        
        Here is a wikilink image without alt text:
        
        ![[assets/image.png]]
        
        End of document.
        """
    )
    _write_md(tmp_path, md_content, "wikilink.md")

    queue = scan.build_queue(tmp_path)
    assert len(queue) == 1
    assert queue[0].asset_path == "assets/image.png"
    assert "Here is a wikilink image" in queue[0].context_snippet




def test_wikilink_line_numbers(tmp_path: Path) -> None:
    """Test that line numbers are correctly identified for wikilink images."""
    md_content = textwrap.dedent(
        """
        ---
        title: Test
        ---
        
        First paragraph.
        
        ![[image1.png]]
        
        Second paragraph.
        
        ![[image2.png]]
        
        Third paragraph.
        """
    )
    file_path = _write_md(tmp_path, md_content, "line_numbers.md")

    queue = scan.build_queue(tmp_path)
    assert len(queue) == 2

    # Find actual line numbers in the file
    lines = file_path.read_text().splitlines()
    image1_line = next(i + 1 for i, line in enumerate(lines) if "image1.png" in line)
    image2_line = next(i + 1 for i, line in enumerate(lines) if "image2.png" in line)

    # Check that detected line numbers match
    line_numbers = {item.line_number for item in queue}
    assert image1_line in line_numbers
    assert image2_line in line_numbers


@pytest.mark.parametrize(
    "content,expected_asset",
    [
        pytest.param(
            "![[output-feedback#]]\n![[another#heading]]\n![[diagram.png#light-mode]]\n![[photo.jpg]]",
            "photo.jpg",
            id="document_embeds_with_hash_ignored",
        ),
        pytest.param(
            "![[document.pdf]]\n![[notes.md]]\n![[readme.txt]]\n![[photo.png]]",
            "photo.png",
            id="non_image_files_ignored",
        ),
    ],
)
def test_wikilink_non_image_wikilinks_ignored(
    tmp_path: Path, content: str, expected_asset: str
) -> None:
    """Non-image wikilinks (document embeds, non-media files) should be ignored."""
    _write_md(tmp_path, content, "test.md")
    queue = scan.build_queue(tmp_path)
    assert len(queue) == 1
    assert queue[0].asset_path == expected_asset


@pytest.mark.parametrize(
    "extension", sorted([ext.lstrip(".") for ext in scan.WIKILINK_ASSET_EXTENSIONS])
)
def test_all_image_extensions_recognized(tmp_path: Path, extension: str) -> None:
    """Test that all IMAGE_EXTENSIONS are properly recognized as images in wikilinks."""
    filename = f"test_image.{extension}"
    md_content = f"![[{filename}]]"
    _write_md(tmp_path, md_content, "extension_test.md")

    queue = scan.build_queue(tmp_path)

    # Should find the image regardless of extension
    assert len(queue) == 1
    assert queue[0].asset_path == filename


@pytest.mark.parametrize(
    "extension", sorted([ext.lstrip(".") for ext in scan.WIKILINK_ASSET_EXTENSIONS])
)
def test_image_extensions_with_fragment_not_recognized(
    tmp_path: Path, extension: str
) -> None:
    """Test that wikilinks with # fragments are NOT treated as images (they're document embeds)."""
    filename = f"diagram.{extension}#light-mode"
    md_content = f"![[{filename}]]"
    _write_md(tmp_path, md_content, "fragment_test.md")

    queue = scan.build_queue(tmp_path)

    # Should NOT find images with # fragments - they're document embeds
    assert len(queue) == 0


@pytest.mark.parametrize(
    "label,expected",
    [
        (None, False),
        ("", False),
        ("   ", False),
        ("video", False),
        ("movie", False),
        ("VIDEO", False),
        ("A meaningful video description", True),
    ],
)
def test_is_video_label_meaningful(label: str | None, expected: bool):
    assert scan._is_video_label_meaningful(label) == expected


@pytest.mark.parametrize(
    "video_html,should_detect",
    [
        ('<video src="demo.mp4"></video>', True),
        ('<video src="demo.mp4" aria-label="Good description"></video>', False),
        ('<video src="demo.mp4" title="Tutorial"></video>', False),
        ('<video src="demo.mp4" aria-describedby="desc"></video>', False),
        ('<video src="demo.mp4" aria-label="video"></video>', True),
        ("<video src='demo.mp4'></video>", True),
    ],
)
def test_video_detection(tmp_path: Path, video_html: str, should_detect: bool):
    md_file = tmp_path / "test.md"
    md_file.write_text(f"{video_html}\n")

    queue = scan.build_queue(tmp_path)
    if should_detect:
        assert len(queue) == 1
        assert queue[0].asset_path == "demo.mp4"
    else:
        assert len(queue) == 0


def test_mixed_images_and_videos(tmp_path: Path):
    md_file = tmp_path / "test.md"
    md_file.write_text(
        "![](image.png)\n" '<video src="demo.mp4"></video>\n' '<img src="photo.jpg">\n'
    )

    queue = scan.build_queue(tmp_path)
    assert len(queue) == 3
    paths = {item.asset_path for item in queue}
    assert paths == {"image.png", "demo.mp4", "photo.jpg"}


# ---------------------------------------------------------------------------
# Edge cases and boundary conditions
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "content",
    [
        pytest.param("", id="empty_file"),
        pytest.param("---\ntitle: hello\n---\n", id="only_frontmatter"),
    ],
)
def test_no_images_produces_empty_queue(tmp_path: Path, content: str) -> None:
    """Files without images should produce no queue items."""
    (tmp_path / "test.md").write_text(content, encoding="utf-8")
    assert scan.build_queue(tmp_path) == []


def test_hundreds_of_images(tmp_path: Path) -> None:
    """File with 500 images should all be detected."""
    lines = [f"![](image_{i}.png)" for i in range(500)]
    (tmp_path / "big.md").write_text("\n\n".join(lines), encoding="utf-8")
    assert len(scan.build_queue(tmp_path)) == 500


@pytest.mark.parametrize(
    "content,expected,excluded",
    [
        pytest.param(
            "# Docs\n\n```markdown\n![](code.png)\n```\n\n![](real.png)\n",
            {"real.png"},
            {"code.png"},
            id="fenced_code_block",
        ),
        pytest.param(
            "Use `![](fake.png)` syntax.\n\n![](real.png)\n",
            {"real.png"},
            {"fake.png"},
            id="inline_code",
        ),
    ],
)
def test_images_in_code_ignored(
    tmp_path: Path, content: str, expected: set[str], excluded: set[str]
) -> None:
    """Images inside code contexts should NOT be detected."""
    (tmp_path / "test.md").write_text(content, encoding="utf-8")
    paths = {item.asset_path for item in scan.build_queue(tmp_path)}
    assert expected <= paths
    assert paths.isdisjoint(excluded)


@pytest.mark.parametrize(
    "content,expected_path,expected_count",
    [
        pytest.param("![](画像.png)\n", "画像.png", 1, id="unicode_md_path"),
        pytest.param('<img src="фото.jpg">\n', "фото.jpg", 1, id="unicode_html_path"),
        pytest.param("![日本語の説明](image.png)\n", None, 0, id="unicode_alt_meaningful"),
    ],
)
def test_unicode_handling(
    tmp_path: Path, content: str, expected_path: str | None, expected_count: int
) -> None:
    """Unicode in asset paths should be detected; unicode alt text is meaningful."""
    (tmp_path / "test.md").write_text(content, encoding="utf-8")
    queue = scan.build_queue(tmp_path)
    assert len(queue) == expected_count
    if expected_path is not None:
        assert queue[0].asset_path == expected_path


@pytest.mark.parametrize(
    "content,expected_asset",
    [
        pytest.param(
            '<div><div><div><p><img src="nested.jpg"></p></div></div></div>\n',
            "nested.jpg",
            id="deeply_nested",
        ),
        pytest.param(
            '<img src="ok.png">\n<img src="broken.jpg\n<img src=unclosed>\n',
            "ok.png",
            id="malformed_html",
        ),
        pytest.param(
            '<img src="image (1).png">\n',
            "image (1).png",
            id="special_regex_chars",
        ),
    ],
)
def test_html_edge_cases(tmp_path: Path, content: str, expected_asset: str) -> None:
    """Various HTML edge cases should detect the expected asset."""
    (tmp_path / "test.md").write_text(content, encoding="utf-8")
    paths = {item.asset_path for item in scan.build_queue(tmp_path)}
    assert expected_asset in paths


def test_video_with_source_child(tmp_path: Path) -> None:
    """Video tag using <source> child instead of src attribute."""
    content = textwrap.dedent("""\
        # Video Demo

        Some intro text.

        <video controls><source src="clip.mp4" type="video/mp4"></video>

        After video text.
    """)
    (tmp_path / "source_video.md").write_text(content, encoding="utf-8")
    queue = scan.build_queue(tmp_path)
    assert any(item.asset_path == "clip.mp4" for item in queue)


def test_multiple_files_in_directory(tmp_path: Path) -> None:
    """Scanning a directory with multiple markdown files."""
    for i in range(10):
        (tmp_path / f"file_{i}.md").write_text(
            f"![](image_{i}.png)\n", encoding="utf-8"
        )
    assert len(scan.build_queue(tmp_path)) == 10


def test_non_md_files_ignored(tmp_path: Path) -> None:
    """Non-markdown files should be ignored."""
    (tmp_path / "image.png").write_bytes(b"\x89PNG")
    (tmp_path / "readme.txt").write_text("![](img.png)")
    (tmp_path / "actual.md").write_text("![](real.png)\n")
    queue = scan.build_queue(tmp_path)
    assert len(queue) == 1
    assert queue[0].asset_path == "real.png"


def test_url_assets_detected(tmp_path: Path) -> None:
    """Remote URL assets should be detected."""
    (tmp_path / "url.md").write_text(
        "![](https://example.com/img.jpg)\n", encoding="utf-8"
    )
    queue = scan.build_queue(tmp_path)
    assert len(queue) == 1
    assert queue[0].asset_path == "https://example.com/img.jpg"
