"""Tests for gcode_compat — generic slicer G-code → BBL firmware translation."""

from bambox.gcode_compat import (
    _build_header_block,
    _parse_prusa_time,
    is_bbl_gcode,
    translate_to_bbl,
)

# --- is_bbl_gcode ---


def test_orca_gcode_detected():
    gcode = b"; HEADER_BLOCK_START\n; total layer number: 50\n; HEADER_BLOCK_END\n"
    assert is_bbl_gcode(gcode) is True


def test_cura_gcode_not_bbl():
    gcode = b";FLAVOR:Marlin\n;LAYER_COUNT:50\n;LAYER:0\n"
    assert is_bbl_gcode(gcode) is False


def test_prusa_gcode_not_bbl():
    gcode = b"; generated by PrusaSlicer\n;LAYER_CHANGE\n;HEIGHT:0.2\n"
    assert is_bbl_gcode(gcode) is False


def test_accepts_str():
    assert is_bbl_gcode("; HEADER_BLOCK_START\n; total layer number: 1\n") is True


# --- translate_to_bbl (passthrough) ---


def test_bbl_gcode_unchanged():
    gcode = b"; HEADER_BLOCK_START\n; total layer number: 50\n; HEADER_BLOCK_END\nG28\n"
    assert translate_to_bbl(gcode) == gcode


# --- translate_to_bbl (CuraEngine) ---


CURA_GCODE = (
    b";FLAVOR:Marlin\n"
    b";TIME:338\n"
    b";Filament used: 0.33m\n"
    b";Layer height: 0.2\n"
    b";MAXZ:10\n"
    b";TARGET_MACHINE.NAME:BambuLab P1S\n"
    b"\n"
    b";LAYER_COUNT:3\n"
    b";LAYER:0\n"
    b"G1 Z0.2 F3000\n"
    b"G1 X10 Y10 E1\n"
    b";LAYER:1\n"
    b"G1 Z0.4\n"
    b";LAYER:2\n"
    b"G1 Z0.6\n"
)


def test_cura_header_block():
    result = translate_to_bbl(CURA_GCODE).decode()
    assert result.startswith("; HEADER_BLOCK_START\n")
    assert "; HEADER_BLOCK_END\n" in result
    assert "; total layer number: 3" in result
    assert "; max_z_height: 10" in result
    assert "; total estimated time: 5m 38s" in result


def test_cura_layer_markers():
    result = translate_to_bbl(CURA_GCODE).decode()

    # Layer 0 → 1/3
    assert "; layer num/total_layer_count: 1/3" in result
    assert "M73 L1\n" in result
    assert "M991 S0 P0 ;notify layer change" in result

    # Layer 2 → 3/3
    assert "; layer num/total_layer_count: 3/3" in result
    assert "M73 L3\n" in result
    assert "M991 S0 P2 ;notify layer change" in result


def test_cura_progress_commands():
    """M73 P{pct} R{remaining_min} injected at each layer."""
    result = translate_to_bbl(CURA_GCODE).decode()
    # Layer 0: 0% done, full time remaining (338s ≈ 6min)
    assert "M73 P0 R6\n" in result
    # Layer 1: 33% done
    assert "M73 P33 R4\n" in result
    # Layer 2: 67% done
    assert "M73 P67 R2\n" in result


def test_cura_original_markers_preserved():
    result = translate_to_bbl(CURA_GCODE).decode()
    assert ";LAYER_COUNT:3" in result
    assert ";LAYER:0" in result
    assert ";LAYER:2" in result


def test_cura_movement_preserved():
    result = translate_to_bbl(CURA_GCODE).decode()
    assert "G1 Z0.2 F3000" in result
    assert "G1 X10 Y10 E1" in result


# --- translate_to_bbl (PrusaSlicer) ---


PRUSA_GCODE = (
    b"; generated by PrusaSlicer 2.7.0\n"
    b"; estimated printing time (normal mode) = 1h 5m 30s\n"
    b";LAYER_CHANGE\n"
    b";HEIGHT:0.2\n"
    b"G1 Z0.2 F3000\n"
    b";LAYER_CHANGE\n"
    b";HEIGHT:0.4\n"
    b"G1 Z0.4\n"
    b";LAYER_CHANGE\n"
    b";HEIGHT:0.6\n"
    b"G1 Z0.6\n"
)


def test_prusa_header_block():
    result = translate_to_bbl(PRUSA_GCODE).decode()
    assert result.startswith("; HEADER_BLOCK_START\n")
    assert "; total layer number: 3" in result
    assert "; max_z_height: 0.6" in result
    assert "; total estimated time: 1h 5m 30s" in result


def test_prusa_layer_markers():
    result = translate_to_bbl(PRUSA_GCODE).decode()
    assert "; layer num/total_layer_count: 1/3" in result
    assert "M73 L1\n" in result
    assert "M991 S0 P0 ;notify layer change" in result
    assert "; layer num/total_layer_count: 3/3" in result
    assert "M73 L3\n" in result


def test_prusa_progress_commands():
    """M73 P{pct} R{remaining_min} injected at each layer."""
    result = translate_to_bbl(PRUSA_GCODE).decode()
    # Layer 0: 0% done, full time remaining (3930s ≈ 66min)
    assert "M73 P0 R66\n" in result
    # Layer 2: 67% done
    assert "M73 P67 R22\n" in result


def test_prusa_original_markers_preserved():
    result = translate_to_bbl(PRUSA_GCODE).decode()
    assert ";LAYER_CHANGE" in result
    assert ";HEIGHT:0.2" in result


# --- Z-change fallback (unknown slicer) ---


def test_zchange_plain_gcode_gets_layer_markers():
    """Plain G-code with Z moves and no slicer comments should get layer markers."""
    gcode = b"G28\nG1 X0 Y0 Z0.2 F3000\nG1 X10 Y10 E1\nG1 Z0.4\nG1 X20 Y20 E2\nG0 Z0.6\nG1 X30 E3\n"
    result = translate_to_bbl(gcode).decode()
    assert result.startswith("; HEADER_BLOCK_START\n")
    assert "; total layer number: 3" in result
    assert "; max_z_height: 0.60" in result
    assert "M73 L1\n" in result
    assert "M73 L2\n" in result
    assert "M73 L3\n" in result
    assert "M991 S0 P0 ;notify layer change" in result
    assert "M991 S0 P2 ;notify layer change" in result
    # Original movement commands preserved
    assert "G1 X10 Y10 E1" in result


def test_zchange_no_z_moves_unchanged():
    """G-code with no Z moves should be returned unchanged."""
    gcode = b"G28\nG1 X10 Y10 E1 F300\nG1 X20 Y20 E2\n"
    assert translate_to_bbl(gcode) == gcode


def test_zchange_mixed_g0_g1():
    """Both G0 and G1 Z changes should be detected."""
    gcode = b"G1 Z0.2 F3000\nG0 Z0.4\nG1 Z0.6\n"
    result = translate_to_bbl(gcode).decode()
    assert "; total layer number: 3" in result
    assert "M73 L1\n" in result
    assert "M73 L2\n" in result
    assert "M73 L3\n" in result


def test_zchange_z_decrease_not_counted():
    """Z decreases (e.g. retract or Z-hop return) should not count as new layers."""
    gcode = b"G1 Z0.2 F3000\nG1 Z0.4\nG1 Z0.3\nG1 Z0.6\n"
    result = translate_to_bbl(gcode).decode()
    # Z0.3 < Z0.4 so not a new layer; only 0.2, 0.4, 0.6 count
    assert "; total layer number: 3" in result


def test_zchange_does_not_fire_for_cura():
    """Fallback must not fire when CuraEngine markers are present."""
    result = translate_to_bbl(CURA_GCODE).decode()
    # Should use Cura path (has ;LAYER_COUNT:3), not zchange
    assert ";LAYER_COUNT:3" in result
    assert "; total layer number: 3" in result


def test_zchange_does_not_fire_for_prusa():
    """Fallback must not fire when PrusaSlicer markers are present."""
    result = translate_to_bbl(PRUSA_GCODE).decode()
    assert ";LAYER_CHANGE" in result
    assert "; total layer number: 3" in result


def test_zchange_progress_percentage():
    """Progress percentage should increase with each layer."""
    gcode = b"G1 Z0.2\nG1 Z0.4\nG1 Z0.6\nG1 Z0.8\n"
    result = translate_to_bbl(gcode).decode()
    assert "M73 P0 R0\n" in result  # layer 0
    assert "M73 P25 R0\n" in result  # layer 1
    assert "M73 P50 R0\n" in result  # layer 2
    assert "M73 P75 R0\n" in result  # layer 3


# --- Helpers ---


def test_build_header_block():
    block = _build_header_block(50, 338, "10")
    assert "; HEADER_BLOCK_START" in block
    assert "; total layer number: 50" in block
    assert "; total estimated time: 5m 38s" in block
    assert "; max_z_height: 10" in block
    assert "; HEADER_BLOCK_END" in block


def test_build_header_block_hours():
    block = _build_header_block(100, 3661, "20")
    assert "1h 1m 1s" in block


def test_parse_prusa_time():
    assert _parse_prusa_time("1h 5m 30s") == 3930
    assert _parse_prusa_time("23m 10s") == 1390
    assert _parse_prusa_time("45s") == 45
    assert _parse_prusa_time("2h 0m 0s") == 7200


# --- Integration: pack_gcode_3mf auto-translates ---


def test_pack_auto_translates_cura(tmp_path):
    """pack_gcode_3mf automatically translates CuraEngine G-code."""
    import zipfile

    from bambox.pack import pack_gcode_3mf

    out = tmp_path / "test.gcode.3mf"
    pack_gcode_3mf(CURA_GCODE, out)

    with zipfile.ZipFile(out) as z:
        inner = z.read("Metadata/plate_1.gcode").decode()
        assert inner.startswith("; HEADER_BLOCK_START")
        assert "; total layer number: 3" in inner
        assert "M73 L1\n" in inner
        assert "M991 S0 P0" in inner


def test_pack_no_double_translate(tmp_path):
    """pack_gcode_3mf does not double-translate BBL G-code."""
    import zipfile

    from bambox.pack import pack_gcode_3mf

    bbl_gcode = b"; HEADER_BLOCK_START\n; total layer number: 2\n; HEADER_BLOCK_END\nG28\nG1 Z0.2\n"
    out = tmp_path / "test.gcode.3mf"
    pack_gcode_3mf(bbl_gcode, out)

    with zipfile.ZipFile(out) as z:
        inner = z.read("Metadata/plate_1.gcode").decode()
        # Should appear exactly once
        assert inner.count("; HEADER_BLOCK_START") == 1


# --- Cura edge cases (uncovered lines) ---


def test_cura_no_layer_count_returns_unchanged():
    """Cura path with no ;LAYER_COUNT: should return text unchanged (line 86)."""
    gcode = b";FLAVOR:Marlin\n;LAYER:0\nG1 Z0.2\n"
    # Has ;LAYER: but not ;LAYER_COUNT:, so _translate_cura returns early
    # However translate_to_bbl routes to zchange fallback, not _translate_cura
    # Test _translate_cura directly
    from bambox.gcode_compat import _translate_cura

    text = gcode.decode()
    result = _translate_cura(text)
    assert result == text


def test_cura_with_time_elapsed():
    """Cura G-code with TIME_ELAPSED uses accurate per-layer timing (lines 92, 105, 128)."""
    gcode = (
        b";LAYER_COUNT:3\n"
        b";TIME:6666\n"
        b";Layer height: 0.2\n"
        b";MAXZ:0.6\n"
        b";LAYER:0\n"
        b"G1 Z0.2\n"
        b";TIME_ELAPSED:10.5\n"
        b";LAYER:1\n"
        b"G1 Z0.4\n"
        b";TIME_ELAPSED:20.0\n"
        b";LAYER:2\n"
        b"G1 Z0.6\n"
        b";TIME_ELAPSED:30.0\n"
    )
    result = translate_to_bbl(gcode).decode()
    # Should use last TIME_ELAPSED (30s) as total time, not ;TIME:6666
    assert "; total estimated time: 0m 30s" in result
    # Layer markers should be present
    assert "M73 L1\n" in result


def test_cura_negative_maxz_uses_layer_height():
    """Cura with negative/zero MAXZ computes from layer count (lines 112-114)."""
    gcode = (
        b";LAYER_COUNT:5\n;Layer height: 0.2\n;MAXZ:-2.14748e+06\n;TIME:100\n;LAYER:0\nG1 Z0.2\n"
    )
    result = translate_to_bbl(gcode).decode()
    # 5 layers * 0.2mm = 1.0
    assert "; max_z_height: 1.0" in result


def test_cura_no_maxz_no_layer_height():
    """Cura with no MAXZ and no layer height uses 0.2 default (line 113)."""
    gcode = b";LAYER_COUNT:10\n;TIME:100\n;LAYER:0\nG1 Z0.2\n"
    result = translate_to_bbl(gcode).decode()
    # 10 layers * 0.2 default = 2.0
    assert "; max_z_height: 2.0" in result


def test_cura_total_zero_layers():
    """Edge case: total=0 should not divide by zero (line 132)."""
    from bambox.gcode_compat import _translate_cura

    # Construct text that has LAYER_COUNT:0
    text = ";LAYER_COUNT:0\n;TIME:100\n;MAXZ:1.0\n;LAYER:0\nG1 Z0.2\n"
    result = _translate_cura(text)
    # Should not crash, layer 0 should get 0% progress
    assert "M73 P0" in result


# --- Prusa edge cases ---


def test_prusa_no_time_estimate():
    """Prusa G-code without time estimate should use 0 (line 158/162)."""
    gcode = b"; generated by PrusaSlicer\n;LAYER_CHANGE\n;HEIGHT:0.2\nG1 Z0.2\n"
    result = translate_to_bbl(gcode).decode()
    assert "; total estimated time: 0m 0s" in result


def test_prusa_maxz_from_last_height():
    """Prusa without ;MAXZ: uses last ;HEIGHT: value (line 170)."""
    gcode = (
        b"; generated by PrusaSlicer\n"
        b";LAYER_CHANGE\n;HEIGHT:0.2\nG1 Z0.2\n"
        b";LAYER_CHANGE\n;HEIGHT:0.4\nG1 Z0.4\n"
        b";LAYER_CHANGE\n;HEIGHT:3.6\nG1 Z3.6\n"
    )
    result = translate_to_bbl(gcode).decode()
    assert "; max_z_height: 3.6" in result
