import argparse
import io
import logging
import struct
from pathlib import Path

import bincopy
import pytest
from serial import Serial

import mcbootflash as bf
import mcbootflash.__main__ as main
import mcbootflash.flash
import mcbootflash.protocol

PORTNAME = "/dev/ttyUSB0"
BAUDRATE = 460800


@pytest.fixture()
def connection():
    return Serial(port=PORTNAME, baudrate=BAUDRATE, timeout=1)


def test_wrong_packet():
    resp = mcbootflash.protocol.Checksum(mcbootflash.protocol.CommandCode.CALC_CHECKSUM)
    with pytest.raises(struct.error) as excinfo:
        mcbootflash.protocol.Version.unpack(bytes(resp))
    assert "requires 37 bytes, got 14" in str(excinfo)


@pytest.mark.parametrize(
    ("debug", "quiet"),
    [(False, False), (True, False), (False, True)],
)
def test_cli(reserial, caplog, debug, quiet):
    caplog.set_level(logging.INFO)
    main.main(
        argparse.Namespace(
            hexfile="tests/testcases/flash/test.hex",
            port=PORTNAME,
            baudrate=BAUDRATE,
            timeout=1,
            checksum=True,
            reset=False,
            debug=debug,
            quiet=quiet,
        ),
    )
    assert "Self verify OK" in caplog.messages[-1]


def test_cli_error(caplog):
    if Path(PORTNAME).exists():
        msg = f"{PORTNAME} exists: skipping device not connected test"
        pytest.skip(msg)

    caplog.set_level(logging.INFO)
    main.main(
        argparse.Namespace(
            hexfile="tests/testcases/flash/test.hex",
            port=PORTNAME,
            baudrate=BAUDRATE,
            timeout=10,
            checksum=True,
            reset=False,
            debug=True,
            quiet=False,
        ),
    )
    assert caplog.records[-1].levelno == logging.ERROR


def test_get_parser():
    parser = main.get_parser()
    assert parser.description == (
        "mcbootflash is a tool for flashing firmware to 16-bit Microchip MCUs and DSCs "
        "from the PIC24 and dsPIC33 device families, which are running a bootloader "
        "generated by the MPLAB Code Configurator tool."
    )


def test_datasize_large():
    assert main.get_datasize(2**20) == "1.0 MiB"


def test_progressbar(capsys):
    import logging

    info_handler = logging.Handler(level=logging.INFO)
    main.logger.addHandler(info_handler)
    main.print_progress(100, 200, 5)
    expected = (
        "50%  100 B  |#####################                      |  "
        "Elapsed Time: 0:00:05\r"
    )
    assert capsys.readouterr().out == expected


def test_get_bootattrs(reserial, connection):
    test_device_boot_attrs = bf.BootAttrs(
        version=258,
        max_packet_length=256,
        device_id=13398,
        erase_size=2048,
        write_size=8,
        memory_range=(6144, 174080),
    )
    assert bf.get_boot_attrs(connection) == test_device_boot_attrs


def test_erase(reserial, caplog, connection):
    caplog.set_level(logging.DEBUG)
    bootattrs = bf.get_boot_attrs(connection)
    connection.timeout = 10
    bf.erase_flash(connection, bootattrs.memory_range, bootattrs.erase_size)
    assert "Erasing addresses" in caplog.messages[-4]


def test_erase_misaligned():
    with pytest.raises(ValueError) as excinfo:
        bf.erase_flash(Serial(), (0, 1), 2)
    assert "not a multiple" in str(excinfo.value)


def test_verify_fail(reserial, connection):
    with pytest.raises(bf.VerifyFail):
        bf.self_verify(connection)


def test_checksum_error(reserial, connection):
    bootattrs = bf.get_boot_attrs(connection)
    chunk = bincopy.Segment(
        minimum_address=bootattrs.memory_range[0],
        maximum_address=bootattrs.memory_range[0] + bootattrs.write_size,
        data=bytes(bootattrs.write_size),
        word_size_bytes=1,
    )
    with pytest.raises(bf.BootloaderError) as excinfo:
        bf.checksum(connection, chunk)
    assert "Checksum mismatch" in str(excinfo.value)


def test_no_data():
    bootattrs = bf.BootAttrs(
        version=258,
        max_packet_length=256,
        device_id=13398,
        erase_size=2048,
        write_size=8,
        memory_range=(6144, 174080),
    )
    with pytest.raises(bincopy.Error) as excinfo:
        bf.chunked("tests/testcases/no_data/test.hex", bootattrs)
    assert "no data" in str(excinfo.value)


def test_unexpected_response(reserial, connection):
    if Path(PORTNAME).exists():
        # Unexpected response uses synthetic data.
        msg = f"{PORTNAME} exists: skipping unexpected response test"
        pytest.skip(msg)

    with pytest.raises(bf.BootloaderError) as excinfo:
        bf.reset(connection)
    assert "Command code mismatch" in str(excinfo.value)


def test_readback(reserial, connection):
    boot_attrs = bf.get_boot_attrs(connection)
    expected = bincopy.BinFile()
    expected.add_microchip_hex_file("tests/testcases/flash/test.hex")
    expected.crop(*boot_attrs.memory_range)
    f = io.StringIO()
    bf.readback(connection, f)
    data = bincopy.BinFile()
    data.add_microchip_hex(f.getvalue())
    assert data[: expected.maximum_address] == expected[:]


def test_format_debug_bytes():
    testbytes_tx = b"0123"
    testbytes_rx = b"456789"
    formatted_tx = mcbootflash.flash._format_debug_bytes(testbytes_tx)
    formatted_rx = mcbootflash.flash._format_debug_bytes(testbytes_rx, testbytes_tx)
    expected = "30 31 32 33\n            34 35 36 37 38 39"
    assert formatted_tx + "\n" + formatted_rx == expected


def test_checksum_bad_address_warning(reserial, caplog, connection):
    boot_attrs = bf.get_boot_attrs(connection)
    payload_size = 240
    chunk = bincopy.Segment(
        minimum_address=boot_attrs.memory_range[1] - payload_size // 2,
        maximum_address=boot_attrs.memory_range[1],
        data=b"\xff" * payload_size,
        word_size_bytes=1,
    )
    bf.checksum(connection, chunk)
    assert "BAD_ADDRESS" in caplog.messages[-3]


def test_reset(reserial, caplog, connection):
    caplog.set_level(logging.DEBUG)
    bf.reset(connection)
    assert "Device reset" in caplog.messages[-1]
