"""Tests for src/cli/main.py."""

from PIL import Image
import pikepdf
import pytest
from click.testing import CliRunner

from src.cli.main import _parse_fuse_config, cli


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_pdf(path, pages=1, title=None):
    pdf = pikepdf.Pdf.new()
    for _ in range(pages):
        pdf.pages.append(pikepdf.Page(pikepdf.Dictionary(Type=pikepdf.Name("/Page"))))
    if title:
        pdf.docinfo["/Title"] = title
    pdf.save(str(path))
    return str(path)


def _make_image(path, color=(100, 150, 200)):
    Image.new("RGB", (200, 150), color).save(str(path))
    return str(path)


def test_help():
    runner = CliRunner()
    result = runner.invoke(cli, ["--help"])
    assert result.exit_code == 0
    assert "assemble" in result.output
    assert "info" in result.output
    assert "init" in result.output


def test_init_creates_file(tmp_path):
    runner = CliRunner()
    config = tmp_path / "config.fuse"
    result = runner.invoke(cli, ["init", "--output", str(config)])
    assert result.exit_code == 0, result.output
    assert config.exists()
    content = config.read_text()
    assert "output: assembled.pdf" in content
    assert "from: document.pdf" in content


def test_parse_fuse_config_paths(tmp_path):
    config = tmp_path / "test.fuse"
    config.write_text(
        "output: out.pdf\n"
        "from: reports/summary.pdf         1-2\n"       # subdir path
        "from: My Documents/cover.pdf      1\n"          # spaces in dir name
        "from: /absolute/path/appendix.pdf all\n"        # absolute path
        "from: ../sibling/notes.pdf        3\n"          # relative with ..
        "from: plain.pdf\n"                              # no pages → defaults to all
    )
    outputs, sources, _ = _parse_fuse_config(str(config))
    assert outputs == ["out.pdf"]
    assert sources[0] == {"file": "reports/summary.pdf", "pages": "1-2"}
    assert sources[1] == {"file": "My Documents/cover.pdf", "pages": "1"}
    assert sources[2] == {"file": "/absolute/path/appendix.pdf", "pages": "all"}
    assert sources[3] == {"file": "../sibling/notes.pdf", "pages": "3"}
    assert sources[4] == {"file": "plain.pdf", "pages": "all"}


# ---------------------------------------------------------------------------
# .fuse format correctness
# ---------------------------------------------------------------------------

def _fuse(tmp_path, content):
    """Write a .fuse config and return its path."""
    p = tmp_path / "test.fuse"
    p.write_text(content)
    return str(p)


def test_fuse_comments_and_blank_lines_ignored(tmp_path):
    outputs, sources, _ = _parse_fuse_config(_fuse(tmp_path,
        "# this is a comment\n"
        "\n"
        "output: out.pdf\n"
        "\n"
        "# another comment\n"
        "from: a.pdf    1\n"
        "\n"
    ))
    assert outputs == ["out.pdf"]
    assert len(sources) == 1


def test_fuse_output_whitespace_stripped(tmp_path):
    outputs, _, _meta = _parse_fuse_config(_fuse(tmp_path,
        "output:   spaced.pdf   \n"
        "from: a.pdf\n"
    ))
    assert outputs == ["spaced.pdf"]


def test_fuse_multiple_outputs_order_preserved(tmp_path):
    outputs, _, _meta = _parse_fuse_config(_fuse(tmp_path,
        "output: first.pdf\n"
        "output: second.docx\n"
        "output: third.png\n"
        "from: a.pdf\n"
    ))
    assert outputs == ["first.pdf", "second.docx", "third.png"]


def test_fuse_file_order_preserved(tmp_path):
    _, sources, _meta = _parse_fuse_config(_fuse(tmp_path,
        "output: out.pdf\n"
        "from: a.pdf    1\n"
        "from: b.pdf    2\n"
        "from: c.pdf    3\n"
        "from: a.pdf    5\n"  # same file again — must appear twice, in order
    ))
    assert [s["file"] for s in sources] == ["a.pdf", "b.pdf", "c.pdf", "a.pdf"]


def test_fuse_page_specs_parsed_correctly(tmp_path):
    _, sources, _meta = _parse_fuse_config(_fuse(tmp_path,
        "output: out.pdf\n"
        "from: a.pdf    all\n"
        "from: b.pdf    1\n"
        "from: c.pdf    1-3\n"
        "from: d.pdf    1,3,5\n"
        "from: e.pdf    1-3,5,7-9\n"
        "from: f.pdf\n"         # no spec → all
    ))
    assert sources[0]["pages"] == "all"
    assert sources[1]["pages"] == "1"
    assert sources[2]["pages"] == "1-3"
    assert sources[3]["pages"] == "1,3,5"
    assert sources[4]["pages"] == "1-3,5,7-9"
    assert sources[5]["pages"] == "all"


def test_fuse_filename_with_spaces_and_page_spec(tmp_path):
    _, sources, _meta = _parse_fuse_config(_fuse(tmp_path,
        "output: out.pdf\n"
        "from: My Report 2024.pdf    2-4\n"
        "from: Board Pack Final.pdf\n"
    ))
    assert sources[0] == {"file": "My Report 2024.pdf", "pages": "2-4"}
    assert sources[1] == {"file": "Board Pack Final.pdf", "pages": "all"}


def test_fuse_missing_output_raises(tmp_path):
    runner = CliRunner()
    config = _fuse(tmp_path, "from: a.pdf    1\n")
    result = runner.invoke(cli, ["assemble", config])
    assert result.exit_code != 0
    assert "output" in result.output.lower()


def test_fuse_missing_files_raises(tmp_path):
    runner = CliRunner()
    config = _fuse(tmp_path, "output: out.pdf\n")
    result = runner.invoke(cli, ["assemble", config])
    assert result.exit_code != 0
    assert "source" in result.output.lower()


def test_fuse_unknown_lines_ignored(tmp_path):
    """Lines that are neither output:, from:, nor comments should be silently skipped."""
    outputs, sources, _ = _parse_fuse_config(_fuse(tmp_path,
        "output: out.pdf\n"
        "random garbage line\n"
        "from: a.pdf    1\n"
    ))
    assert outputs == ["out.pdf"]
    assert len(sources) == 1


def test_init_refuses_overwrite(tmp_path):
    runner = CliRunner()
    config = tmp_path / "config.fuse"
    config.write_text("output: existing.pdf\n")
    result = runner.invoke(cli, ["init", "--output", str(config)])
    assert result.exit_code != 0
    assert "already exists" in result.output


# ---------------------------------------------------------------------------
# quick — integration
# ---------------------------------------------------------------------------

def test_quick_assembles_pages(tmp_path):
    a = _make_pdf(tmp_path / "a.pdf", pages=3)
    b = _make_pdf(tmp_path / "b.pdf", pages=4)
    out = str(tmp_path / "out.pdf")
    runner = CliRunner()
    result = runner.invoke(cli, ["quick", out, f"{a}:1-2", f"{b}:2,4"])
    assert result.exit_code == 0, result.output
    with pikepdf.open(out) as pdf:
        assert len(pdf.pages) == 4  # 2 from a + 2 from b


def test_quick_defaults_to_all_pages(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=3)
    out = str(tmp_path / "out.pdf")
    result = CliRunner().invoke(cli, ["quick", out, src])
    assert result.exit_code == 0, result.output
    with pikepdf.open(out) as pdf:
        assert len(pdf.pages) == 3


def test_quick_password_protected_pdf(tmp_path):
    pdf = pikepdf.Pdf.new()
    pdf.pages.append(pikepdf.Page(pikepdf.Dictionary(Type=pikepdf.Name("/Page"))))
    protected = str(tmp_path / "protected.pdf")
    pdf.save(protected, encryption=pikepdf.Encryption(owner="owner", user="secret"))

    out = str(tmp_path / "out.pdf")
    result = CliRunner().invoke(cli, ["quick", out, f"{protected}:all"])
    assert result.exit_code != 0
    assert "password" in result.output.lower()


def test_quick_same_stem_images_no_collision(tmp_path):
    """Two images with the same filename from different dirs must both be assembled."""
    dir_a = tmp_path / "a"
    dir_b = tmp_path / "b"
    dir_a.mkdir()
    dir_b.mkdir()
    _make_image(dir_a / "photo.png", color=(255, 0, 0))
    _make_image(dir_b / "photo.png", color=(0, 0, 255))
    out = str(tmp_path / "out.pdf")
    result = CliRunner().invoke(cli, [
        "quick", out,
        f"{dir_a}/photo.png:all",
        f"{dir_b}/photo.png:all",
    ])
    assert result.exit_code == 0, result.output
    with pikepdf.open(out) as pdf:
        assert len(pdf.pages) == 2


# ---------------------------------------------------------------------------
# assemble — integration
# ---------------------------------------------------------------------------

def test_assemble_from_fuse_config(tmp_path):
    a = _make_pdf(tmp_path / "a.pdf", pages=3)
    b = _make_pdf(tmp_path / "b.pdf", pages=5)
    out = tmp_path / "out.pdf"
    config = tmp_path / "test.fuse"
    config.write_text(f"output: {out}\nfrom: {a}    1-2\nfrom: {b}    1,3,5\n")
    result = CliRunner().invoke(cli, ["assemble", str(config)])
    assert result.exit_code == 0, result.output
    with pikepdf.open(str(out)) as pdf:
        assert len(pdf.pages) == 5  # 2 + 3


def test_assemble_multiple_outputs(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=2)
    out1 = tmp_path / "out.pdf"
    out2 = tmp_path / "out2.pdf"
    config = tmp_path / "test.fuse"
    config.write_text(f"output: {out1}\noutput: {out2}\nfrom: {src}    all\n")
    result = CliRunner().invoke(cli, ["assemble", str(config)])
    assert result.exit_code == 0, result.output
    assert out1.exists()
    assert out2.exists()
    with pikepdf.open(str(out1)) as pdf:
        assert len(pdf.pages) == 2
    with pikepdf.open(str(out2)) as pdf:
        assert len(pdf.pages) == 2


def test_assemble_password_protected_pdf(tmp_path):
    pdf = pikepdf.Pdf.new()
    pdf.pages.append(pikepdf.Page(pikepdf.Dictionary(Type=pikepdf.Name("/Page"))))
    protected = str(tmp_path / "protected.pdf")
    pdf.save(protected, encryption=pikepdf.Encryption(owner="owner", user="secret"))

    out = tmp_path / "out.pdf"
    config = tmp_path / "test.fuse"
    config.write_text(f"output: {out}\nfrom: {protected}    all\n")
    result = CliRunner().invoke(cli, ["assemble", str(config)])
    assert result.exit_code != 0
    assert "password" in result.output.lower()


# ---------------------------------------------------------------------------
# split — integration
# ---------------------------------------------------------------------------

def test_split_basic(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=5)
    out1 = str(tmp_path / "part1.pdf")
    out2 = str(tmp_path / "part2.pdf")
    result = CliRunner().invoke(cli, ["split", src, f"{out1}:1-2", f"{out2}:3,5"])
    assert result.exit_code == 0, result.output
    with pikepdf.open(out1) as pdf:
        assert len(pdf.pages) == 2
    with pikepdf.open(out2) as pdf:
        assert len(pdf.pages) == 2


def test_split_defaults_to_all_pages(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=4)
    out = str(tmp_path / "out.pdf")
    result = CliRunner().invoke(cli, ["split", src, out])
    assert result.exit_code == 0, result.output
    with pikepdf.open(out) as pdf:
        assert len(pdf.pages) == 4


def test_split_multiple_outputs_independent(tmp_path):
    """Each output slice must contain only its own pages, not bleed into others."""
    src = _make_pdf(tmp_path / "src.pdf", pages=6)
    outs = [str(tmp_path / f"s{i}.pdf") for i in range(1, 4)]
    result = CliRunner().invoke(cli, [
        "split", src,
        f"{outs[0]}:1",
        f"{outs[1]}:2-4",
        f"{outs[2]}:5-6",
    ])
    assert result.exit_code == 0, result.output
    assert sum(len(pikepdf.open(o).pages) for o in outs) == 6


def test_split_single_page_each(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=3)
    outs = [str(tmp_path / f"p{i}.pdf") for i in range(1, 4)]
    result = CliRunner().invoke(cli, [
        "split", src,
        f"{outs[0]}:1", f"{outs[1]}:2", f"{outs[2]}:3",
    ])
    assert result.exit_code == 0, result.output
    for out in outs:
        with pikepdf.open(out) as pdf:
            assert len(pdf.pages) == 1


# ---------------------------------------------------------------------------
# split — .fuse config mode
# ---------------------------------------------------------------------------

def test_split_fuse_config(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=5)
    out1 = tmp_path / "part1.pdf"
    out2 = tmp_path / "part2.pdf"
    config = tmp_path / "split.fuse"
    config.write_text(
        f"from: {src}\n"
        f"output: {out1}  1-2\n"
        f"output: {out2}  3,5\n"
    )
    result = CliRunner().invoke(cli, ["split", str(config)])
    assert result.exit_code == 0, result.output
    with pikepdf.open(str(out1)) as pdf:
        assert len(pdf.pages) == 2
    with pikepdf.open(str(out2)) as pdf:
        assert len(pdf.pages) == 2


def test_split_fuse_config_defaults_to_all(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=4)
    out = tmp_path / "out.pdf"
    config = tmp_path / "split.fuse"
    config.write_text(f"from: {src}\noutput: {out}\n")
    result = CliRunner().invoke(cli, ["split", str(config)])
    assert result.exit_code == 0, result.output
    with pikepdf.open(str(out)) as pdf:
        assert len(pdf.pages) == 4


def test_split_fuse_config_missing_source(tmp_path):
    out = tmp_path / "out.pdf"
    config = tmp_path / "split.fuse"
    config.write_text(f"output: {out}  1\n")
    result = CliRunner().invoke(cli, ["split", str(config)])
    assert result.exit_code != 0
    assert "from" in result.output.lower()


def test_split_fuse_config_missing_outputs(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=2)
    config = tmp_path / "split.fuse"
    config.write_text(f"from: {src}\n")
    result = CliRunner().invoke(cli, ["split", str(config)])
    assert result.exit_code != 0
    assert "output" in result.output.lower()


def test_split_fuse_config_rejects_inline_mix(tmp_path):
    src = _make_pdf(tmp_path / "src.pdf", pages=2)
    config = tmp_path / "split.fuse"
    config.write_text(f"from: {src}\noutput: {tmp_path}/out.pdf  1\n")
    result = CliRunner().invoke(cli, ["split", str(config), "extra.pdf:1"])
    assert result.exit_code != 0
    assert "mix" in result.output.lower()


# ---------------------------------------------------------------------------
# info — integration
# ---------------------------------------------------------------------------

def test_info_shows_page_count(tmp_path):
    src = _make_pdf(tmp_path / "doc.pdf", pages=7, title="My Report")
    result = CliRunner().invoke(cli, ["info", src])
    assert result.exit_code == 0, result.output
    assert "7" in result.output
    assert "My Report" in result.output


def test_info_shows_format(tmp_path):
    src = _make_pdf(tmp_path / "doc.pdf", pages=2)
    result = CliRunner().invoke(cli, ["info", src])
    assert result.exit_code == 0, result.output
    assert "PDF" in result.output


def test_info_password_protected(tmp_path):
    pdf = pikepdf.Pdf.new()
    pdf.pages.append(pikepdf.Page(pikepdf.Dictionary(Type=pikepdf.Name("/Page"))))
    protected = str(tmp_path / "protected.pdf")
    pdf.save(protected, encryption=pikepdf.Encryption(owner="owner", user="secret"))
    result = CliRunner().invoke(cli, ["info", protected])
    assert result.exit_code != 0
    assert "password" in result.output.lower()
