import json

import pikepdf

from pdftl.operations.dump_layers import _get_active_usage_map, dump_layers, dump_layers_cli_hook


def test_dump_layers_no_ocg(tmp_path):
    """Verify output when no layers exist."""
    pdf = pikepdf.new()
    output_file = tmp_path / "out.json"

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    assert res["has_layers"] is False
    assert res["layers"] == []


def test_dump_layers_basic_hierarchy(tmp_path):
    """Verify a parent-child relationship is correctly parsed."""
    pdf = pikepdf.new()
    output_file = tmp_path / "hierarchy.json"

    # Create OCGs
    ocg1 = pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.OCG, Name="Parent"))
    ocg2 = pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.OCG, Name="Child"))

    # Setup OCProperties
    ocprops = pikepdf.Dictionary(
        OCGs=[ocg1, ocg2],
        D=pikepdf.Dictionary(
            Order=pikepdf.Array([ocg1, pikepdf.Array([ocg2])]),
            BaseState=pikepdf.Name.ON,
        ),
    )
    pdf.Root.OCProperties = ocprops

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    assert res["has_layers"] is True
    parent_id = int(ocg1.objgen[0])
    child_id = int(ocg2.objgen[0])

    # Check tree: [ {parent}, [ {child} ] ]
    assert res["ui_hierarchy"][0]["obj_id"] == parent_id
    assert res["ui_hierarchy"][1][0]["obj_id"] == child_id
    assert res["ui_hierarchy"][1][0]["name"] == "Child"


def test_dump_layers_usage_cleaning(tmp_path):
    """Verify /Usage names like /PrintState become 'PrintState'."""
    pdf = pikepdf.new()
    output_file = tmp_path / "usage.json"

    ocg = pdf.make_indirect(
        pikepdf.Dictionary(
            Type=pikepdf.Name.OCG,
            Name="Layer",
            Usage=pikepdf.Dictionary(Print=pikepdf.Dictionary(PrintState=pikepdf.Name.OFF)),
        )
    )
    pdf.Root.OCProperties = pikepdf.Dictionary(OCGs=[ocg], D=pikepdf.Dictionary())

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    usage = res["layers"][0]["usage"]
    assert "Print" in usage
    assert usage["Print"]["PrintState"] == "OFF"


def test_dump_layers_complex_features(tmp_path):
    """
    Targets:
    - Line 92-95: Labels and Strings in /Order
    - Line 116: Simple value in /Usage
    - Line 156: Alternate /Configs
    - Line 145 & 162: Top-level /Order and fallback logic
    """
    pdf = pikepdf.new()
    output_file = tmp_path / "complex.json"

    # 1. Create OCGs
    ocg1 = pdf.make_indirect(
        pikepdf.Dictionary(
            Type=pikepdf.Name.OCG,
            Name="Layer1",
            Usage=pikepdf.Dictionary(CreatorInfo="AppV1"),  # Line 116: Simple value
        )
    )

    # 2. Setup OCProperties with complex structure
    ocprops = pikepdf.Dictionary(
        OCGs=[ocg1],
        # Line 145: Top-level Order
        Order=pikepdf.Array([pikepdf.String("Section Label"), ocg1]),  # Line 92-93: Label
        D=pikepdf.Dictionary(BaseState=pikepdf.Name.ON),
        # Line 156: Alternate Configs
        Configs=pikepdf.Array([pikepdf.Dictionary(Name="AltView", BaseState=pikepdf.Name.OFF)]),
    )
    pdf.Root.OCProperties = ocprops

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    # Verification
    assert res["ui_hierarchy"][0]["label"] == "Section Label"
    assert res["alternate_configs"][0]["name"] == "AltView"
    assert res["layers"][0]["usage"]["CreatorInfo"] == "AppV1"


def test_dump_layers_legacy_fallback(tmp_path):
    """Targets Line 162: Legacy top-level Order without a /D hierarchy."""
    pdf = pikepdf.new()
    output_file = tmp_path / "legacy.json"

    ocg1 = pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.OCG, Name="Legacy"))

    # OCProperties with Order but NO Order inside D
    pdf.Root.OCProperties = pikepdf.Dictionary(
        OCGs=[ocg1],
        Order=pikepdf.Array([ocg1]),
        D=pikepdf.Dictionary(BaseState=pikepdf.Name.ON),  # No /Order here
    )

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    assert "ui_hierarchy" in res
    assert res["ui_hierarchy"][0]["name"] == "Legacy"


def test_dump_layers_extreme_edge_cases(tmp_path):
    """
    Targets:
    - Line 95: Raw values (int) in /Order
    - Line 162: Legacy Order fallback when /D exists but is empty
    """
    pdf = pikepdf.new()
    output_file = tmp_path / "edge_cases.json"

    ocg1 = pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.OCG, Name="Layer1"))

    # 1. Put an Integer in the Order array to hit Line 95
    # 2. Put Order in Root and an empty D to hit Line 162
    pdf.Root.OCProperties = pikepdf.Dictionary(
        OCGs=[ocg1],
        Order=pikepdf.Array([ocg1, 42]),  # 42 hits Line 95
        D=pikepdf.Dictionary(BaseState=pikepdf.Name.ON),  # No /Order here hits Line 162
    )

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    # Verify Line 95: The integer 42 should be stringified
    assert "42" in res["ui_hierarchy"]
    # Verify Line 162: The hierarchy was still captured
    assert res["ui_hierarchy"][0]["name"] == "Layer1"


def test_get_active_usage_map_coverage():
    # Create a dummy PDF just to generate indirect objects
    pdf = pikepdf.Pdf.new()
    ocg1 = pdf.make_indirect(pikepdf.Dictionary({"/Type": pikepdf.Name("/OCG")}))
    ocg2 = pdf.make_indirect(pikepdf.Dictionary({"/Type": pikepdf.Name("/OCG")}))

    # Build a mock D dictionary with an /AS (Auto-State) array
    d_dict = pikepdf.Dictionary()
    d_dict.AS = pikepdf.Array(
        [
            # 1. A non-dictionary to trigger the `continue` on line 142
            "Not a dictionary, should be skipped",
            # 2. A standard dictionary with an explicit /Category array
            pikepdf.Dictionary(
                {
                    "/Event": pikepdf.Name("/Print"),
                    "/Category": pikepdf.Array([pikepdf.Name("/Print"), pikepdf.Name("/Export")]),
                    "/OCGs": pikepdf.Array([ocg1]),
                }
            ),
            # 3. A dictionary missing /Category to trigger the fallback to /Event on line 151
            pikepdf.Dictionary({"/Event": pikepdf.Name("/View"), "/OCGs": pikepdf.Array([ocg2])}),
        ]
    )

    # Run the function
    result = _get_active_usage_map(d_dict)

    # Assertions
    assert "Print" in result
    assert "Export" in result
    assert "View" in result
    assert int(ocg1.objgen[0]) in result["Print"]
    assert int(ocg1.objgen[0]) in result["Export"]
    assert int(ocg2.objgen[0]) in result["View"]


# ---------------------------------------------------------------------------
# Coverage for previously-untested lines/branches
# ---------------------------------------------------------------------------


def test_dump_layers_usage_custom_category_no_active_flag(tmp_path):
    """Line 126->129: a /Usage sub-dictionary whose key isn't one of the
    standard categories (Print/View/Export/Zoom) is passed through
    without an 'active' flag."""
    pdf = pikepdf.new()
    output_file = tmp_path / "usage_custom.json"

    ocg = pdf.make_indirect(
        pikepdf.Dictionary(
            Type=pikepdf.Name.OCG,
            Name="Layer",
            Usage=pikepdf.Dictionary(CustomCategory=pikepdf.Dictionary(Foo=pikepdf.Name.Bar)),
        )
    )
    pdf.Root.OCProperties = pikepdf.Dictionary(OCGs=[ocg], D=pikepdf.Dictionary())

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    usage = res["layers"][0]["usage"]
    assert "active" not in usage["CustomCategory"]


def test_get_active_usage_map_shared_category():
    """Line 165->167: two /AS entries mapping to the same category merge
    their bound OCG ids into the same, already-existing set."""
    pdf = pikepdf.Pdf.new()
    ocg1 = pdf.make_indirect(pikepdf.Dictionary({"/Type": pikepdf.Name("/OCG")}))
    ocg2 = pdf.make_indirect(pikepdf.Dictionary({"/Type": pikepdf.Name("/OCG")}))

    d_dict = pikepdf.Dictionary()
    d_dict.AS = pikepdf.Array(
        [
            pikepdf.Dictionary({"/Event": pikepdf.Name("/Print"), "/OCGs": pikepdf.Array([ocg1])}),
            pikepdf.Dictionary({"/Event": pikepdf.Name("/Print"), "/OCGs": pikepdf.Array([ocg2])}),
        ]
    )

    result = _get_active_usage_map(d_dict)
    assert int(ocg1.objgen[0]) in result["Print"]
    assert int(ocg2.objgen[0]) in result["Print"]


def test_dump_layers_no_default_config(tmp_path):
    """Line 210: OCProperties with OCGs but no /D at all still produces
    layer data, with an empty active-usage map / default_config."""
    pdf = pikepdf.new()
    output_file = tmp_path / "no_d.json"

    ocg = pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.OCG, Name="Layer"))
    pdf.Root.OCProperties = pikepdf.Dictionary(OCGs=[ocg])

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    assert res["layers"][0]["name"] == "Layer"
    assert res["default_config"] == {}


def test_dump_layers_no_ocgs_key(tmp_path):
    """Line 249->254: OCProperties with a /D but no /OCGs key produces an
    empty layers list without error."""
    pdf = pikepdf.new()
    output_file = tmp_path / "no_ocgs.json"

    pdf.Root.OCProperties = pikepdf.Dictionary(D=pikepdf.Dictionary(BaseState=pikepdf.Name.ON))

    result = dump_layers(pdf, output_file=str(output_file))
    dump_layers_cli_hook(result, None, None)

    with open(output_file) as f:
        res = json.load(f)

    assert res["layers"] == []
