"""
Test module for the lcmlog module.

Authors: Bastián Muñoz and Sebastián Rodríguez-Martínez
Contact: srodriguez@mbari.org
Last Update: 2024-03-26
"""

import pickle
from pathlib import Path

import numpy as np
from compas_lcmtypes.senlcm.dvl_t import dvl_t

from navlib import (
    dict_to_struct,
    lcmlog_to_dict,
    msg_getconstants,
    msg_getfields,
    nav_structtype,
    parse_and_save,
    read_log,
)
from navlib.lcmlog import dict_to_struct as dict_to_struct_relative
from navlib.lcmlog import lcmlog_to_dict as lcmlog_to_dict_relative
from navlib.lcmlog import msg_getconstants as msg_getconstants_relative
from navlib.lcmlog import msg_getfields as msg_getfields_relative
from navlib.lcmlog import nav_structtype as nav_structtype_relative
from navlib.lcmlog import parse_and_save as parse_and_save_relative
from navlib.lcmlog import read_log as read_log_relative

# Define the path to the binary file relative to this test file
BASE_DIR = Path(__file__).resolve().parent
LCMLOG_PATH = BASE_DIR / "resources" / "lcmlog_pytest.00"


def test_imports():
    # Checks that the absolute and relative imports work correctly
    assert lcmlog_to_dict_relative.__code__.co_code == lcmlog_to_dict.__code__.co_code
    assert dict_to_struct_relative.__code__.co_code == dict_to_struct.__code__.co_code
    assert msg_getfields_relative.__code__.co_code == msg_getfields.__code__.co_code
    assert (
        msg_getconstants_relative.__code__.co_code == msg_getconstants.__code__.co_code
    )
    assert parse_and_save_relative.__code__.co_code == parse_and_save.__code__.co_code
    assert read_log_relative.__code__.co_code == read_log.__code__.co_code
    assert nav_structtype_relative == nav_structtype


def test_lcmlog_to_dict():
    """
    Test for the lcmlog_to_dict function.
    """
    # Define a dummy lcmlog_path and opts
    lcmlog_path = LCMLOG_PATH

    # Call the function with the dummy arguments
    result = lcmlog_to_dict(lcmlog_path, verbose=False)

    # Assert that the result is a dictionary
    assert isinstance(result, dict), "Expected result to be a dictionary"

    # Check if types were decoded correctly
    timestamp = result["SIM_MOLA_SEN_IMU"]["header"]["timestamp"][0]
    assert isinstance(timestamp, int), "Expected timestamp to be an int"


def test_dict_to_struct():
    """
    Test for the dict_to_struct function.
    """
    # Define a dummy dictionary
    dummy_dict = {
        "header": {"timestamp": [1, 2, 3]},
        "data": {"value": [4, 5, 6]},
    }

    # Call the function with the dummy arguments
    result = dict_to_struct(dummy_dict)

    # Assert that the result is a nav_structtype object
    assert hasattr(result, "header"), "Expected result to have a header attribute"
    assert hasattr(result, "data"), "Expected result to have a data attribute"

    # Check if types were decoded correctly
    timestamp = result.header.timestamp[0]
    assert isinstance(timestamp, np.int64), "Expected timestamp to be an int"


def test_read_log():
    """
    Test for the read_log function.
    """
    # Define a dummy lcmlog_path and opts
    lcmlog_path = LCMLOG_PATH

    # Call the function with the dummy arguments
    result = read_log(lcmlog_path, verbose=False)

    # Assert that the result is a nav_structtype object
    assert hasattr(
        result, "SIM_MOLA_SEN_IMU"
    ), "Expected result to have a SIM_MOLA_SEN_IMU attribute"

    # Check if types were decoded correctly
    timestamp = result.SIM_MOLA_SEN_IMU.header.timestamp[0]
    assert isinstance(timestamp, np.int64), "Expected timestamp to be an int"

    # Clean up
    output_file = LCMLOG_PATH.with_suffix(".pkl")
    output_file.unlink()


def test_parse_and_save():
    """
    Test for the parse_and_save function.
    """
    # Define a dummy lcmlog_path and opts
    lcmlog_path = LCMLOG_PATH

    # Call the function with the dummy arguments
    parse_and_save(lcmlog_path, verbose=False)

    # Check if the file was created
    output_file = LCMLOG_PATH.with_suffix(".pkl")
    assert output_file.exists(), "Expected output file to exist"

    # Read the pickle file
    with open(output_file, "rb") as f:
        data = pickle.load(f)

    # Assert that the file is not empty
    assert data, "Expected data to be non-empty"

    # Check if types were decoded correctly
    timestamp = data["SIM_MOLA_SEN_IMU"]["header"]["timestamp"][0]
    assert isinstance(timestamp, int), "Expected timestamp to be an int"

    # Remove the file
    output_file.unlink()


def test_msg_getfields():
    """
    Test for the msg_getfields function.
    """
    expected_slots = ["header", "time_sensor", "btv", "wtv", "range", "altitude"]
    slots = msg_getfields(dvl_t)
    assert slots == expected_slots, "Expected slots to be equal"


def test_msg_getconstants():
    """
    Test for the msg_getconstants function.
    """
    expected_constants = ["BTV_SENTINAL", "WTV_SENTINAL"]
    constants = msg_getconstants(dvl_t)
    assert constants == expected_constants, "Expected constants to be equal"


def test_read_log_with_channel_filtering():
    """
    Test for the read_log function with channel filtering.
    """
    # Define a dummy lcmlog_path
    lcmlog_path = LCMLOG_PATH

    # Test 1: Read with specific channel filter
    result_filtered = read_log(
        lcmlog_path, verbose=False, channels_to_process="SIM_MOLA_SEN_IMU"
    )

    # Assert that only the specified channel is present
    assert hasattr(
        result_filtered, "SIM_MOLA_SEN_IMU"
    ), "Expected result to have SIM_MOLA_SEN_IMU attribute"

    # Test 2: Read all channels first to compare
    result_all = read_log(lcmlog_path, verbose=False)

    # Count attributes in both results
    filtered_attrs = [attr for attr in dir(result_filtered) if not attr.startswith("_")]
    all_attrs = [attr for attr in dir(result_all) if not attr.startswith("_")]

    # The filtered result should have fewer or equal attributes
    assert len(filtered_attrs) <= len(
        all_attrs
    ), "Filtered result should have fewer or equal channels"

    # Clean up
    output_file = LCMLOG_PATH.with_suffix(".pkl")
    if output_file.exists():
        output_file.unlink()


def test_filter_channels_from_data():
    """
    Test for the filter_channels_from_data function.
    """
    from navlib.lcmlog.parse_lcmlog import filter_channels_from_data

    # Create a dummy nav_structtype object with multiple channels
    dummy_data = nav_structtype()
    dummy_data.CHANNEL_A = np.array([1, 2, 3])
    dummy_data.CHANNEL_B = np.array([4, 5, 6])
    dummy_data.DIFFERENT_CHANNEL = np.array([7, 8, 9])

    # Test filtering to process only channels starting with "CHANNEL"
    filtered_data = filter_channels_from_data(
        dummy_data, channels_to_process="CHANNEL.*"
    )

    # Check that only CHANNEL_A and CHANNEL_B remain
    assert hasattr(
        filtered_data, "CHANNEL_A"
    ), "Expected filtered data to have CHANNEL_A"
    assert hasattr(
        filtered_data, "CHANNEL_B"
    ), "Expected filtered data to have CHANNEL_B"
    assert not hasattr(
        filtered_data, "DIFFERENT_CHANNEL"
    ), "Expected filtered data to not have DIFFERENT_CHANNEL"

    # Test filtering to ignore specific channels
    filtered_data_ignore = filter_channels_from_data(
        dummy_data, channels_to_ignore="CHANNEL_B"
    )

    # Check that CHANNEL_B is removed but others remain
    assert hasattr(
        filtered_data_ignore, "CHANNEL_A"
    ), "Expected filtered data to have CHANNEL_A"
    assert not hasattr(
        filtered_data_ignore, "CHANNEL_B"
    ), "Expected filtered data to not have CHANNEL_B"
    assert hasattr(
        filtered_data_ignore, "DIFFERENT_CHANNEL"
    ), "Expected filtered data to have DIFFERENT_CHANNEL"


def test_merge_lcm_logs():
    """
    Test the merge_lcm_logs function with actual LCM log files.
    """
    import tempfile

    from lcm import EventLog

    from navlib.lcmlog.parse_lcmlog import merge_lcm_logs

    # Create temporary LCM log files for testing
    with tempfile.TemporaryDirectory() as tmpdir:
        tmpdir_path = Path(tmpdir)

        # Create two test log files with interleaved timestamps
        log1_path = tmpdir_path / "test1.lcm"
        log2_path = tmpdir_path / "test2.lcm"
        merged_path = tmpdir_path / "merged.lcm"

        # Write test data to log1
        log1 = EventLog(str(log1_path), "w")
        log1.write_event(1000000, "CHANNEL_A", b"data1")
        log1.write_event(3000000, "CHANNEL_A", b"data3")
        log1.write_event(5000000, "CHANNEL_B", b"dataB1")
        del log1  # Close the log

        # Write test data to log2
        log2 = EventLog(str(log2_path), "w")
        log2.write_event(2000000, "CHANNEL_A", b"data2")
        log2.write_event(4000000, "CHANNEL_A", b"data4")
        log2.write_event(6000000, "CHANNEL_C", b"dataC1")
        del log2  # Close the log

        # Test 1: Merge without filtering
        merge_lcm_logs(
            [log1_path, log2_path],
            merged_path,
            channels_to_process=".*",
            channels_to_ignore="",
            verbose=False,
        )

        # Read merged log and verify
        merged_log = EventLog(str(merged_path), "r")
        events = list(merged_log)

        # Should have 6 events total, sorted by timestamp
        assert len(events) == 6
        assert events[0].timestamp == 1000000
        assert events[1].timestamp == 2000000
        assert events[2].timestamp == 3000000
        assert events[3].timestamp == 4000000
        assert events[4].timestamp == 5000000
        assert events[5].timestamp == 6000000

        # Check channels
        assert events[0].channel == "CHANNEL_A"
        assert events[1].channel == "CHANNEL_A"
        assert events[4].channel == "CHANNEL_B"
        assert events[5].channel == "CHANNEL_C"

        # Test 2: Merge with channel filtering
        filtered_path = tmpdir_path / "filtered.lcm"
        merge_lcm_logs(
            [log1_path, log2_path],
            filtered_path,
            channels_to_process="CHANNEL_A",
            channels_to_ignore="",
            verbose=False,
        )

        # Read filtered log
        filtered_log = EventLog(str(filtered_path), "r")
        filtered_events = list(filtered_log)

        # Should only have CHANNEL_A events (4 total)
        assert len(filtered_events) == 4
        for event in filtered_events:
            assert event.channel == "CHANNEL_A"

        # Test 3: Merge with ignore pattern
        ignore_path = tmpdir_path / "ignored.lcm"
        merge_lcm_logs(
            [log1_path, log2_path],
            ignore_path,
            channels_to_process=".*",
            channels_to_ignore="CHANNEL_B",
            verbose=False,
        )

        # Read ignored log
        ignored_log = EventLog(str(ignore_path), "r")
        ignored_events = list(ignored_log)

        # Should have all events except CHANNEL_B (5 total)
        assert len(ignored_events) == 5
        for event in ignored_events:
            assert event.channel != "CHANNEL_B"


def test_read_logs():
    """
    Test the read_logs function with multiple log files.
    """
    import shutil
    import tempfile

    from navlib.lcmlog.parse_lcmlog import read_logs

    # Create temporary directory with test log files
    with tempfile.TemporaryDirectory() as tmpdir:
        tmpdir_path = Path(tmpdir)

        # Copy the test log file multiple times to simulate multiple logs
        # We'll use the existing test log file
        test_log = LCMLOG_PATH

        # Create numbered log files
        log1 = tmpdir_path / "lcmlog.00"
        log2 = tmpdir_path / "lcmlog.01"

        shutil.copy(test_log, log1)
        shutil.copy(test_log, log2)

        # Test 1: Read multiple logs without filtering
        result = read_logs(str(tmpdir_path), verbose=False)

        # Should have all channels from the test log
        assert hasattr(result, "SIM_MOLA_SEN_IMU")

        # Test 2: Read with channel filtering
        result_filtered = read_logs(
            str(tmpdir_path),
            channels_to_process="SIM_MOLA_SEN_IMU",
            verbose=False,
        )

        # Should only have the filtered channel
        assert hasattr(result_filtered, "SIM_MOLA_SEN_IMU")
        filtered_attrs = [
            attr for attr in dir(result_filtered) if not attr.startswith("_")
        ]
        # Should have fewer channels than unfiltered
        all_attrs = [attr for attr in dir(result) if not attr.startswith("_")]
        assert len(filtered_attrs) <= len(all_attrs)

        # Clean up generated pickle files
        for pkl_file in tmpdir_path.glob("*.pkl"):
            pkl_file.unlink()
        for lcm_file in tmpdir_path.glob("*.lcm"):
            if lcm_file.name not in ["lcmlog.00", "lcmlog.01"]:
                lcm_file.unlink()


def test_read_logs_single_file():
    """
    Test read_logs with a single log file (should handle without merging).
    """
    import shutil
    import tempfile

    from navlib.lcmlog.parse_lcmlog import read_logs

    with tempfile.TemporaryDirectory() as tmpdir:
        tmpdir_path = Path(tmpdir)

        # Copy single test log
        log1 = tmpdir_path / "lcmlog.00"
        shutil.copy(LCMLOG_PATH, log1)

        # Read single log
        result = read_logs(str(tmpdir_path), verbose=False)

        # Should successfully read the log
        assert hasattr(result, "SIM_MOLA_SEN_IMU")

        # Clean up
        for pkl_file in tmpdir_path.glob("*.pkl"):
            pkl_file.unlink()


def test_read_logs_with_patterns():
    """
    Test read_logs with custom file patterns and ignore patterns.
    """
    import shutil
    import tempfile

    from navlib.lcmlog.parse_lcmlog import read_logs

    with tempfile.TemporaryDirectory() as tmpdir:
        tmpdir_path = Path(tmpdir)

        # Create logs with different naming patterns
        log1 = tmpdir_path / "sensor_data.lcm"
        log2 = tmpdir_path / "debug_data.lcm"
        log3 = tmpdir_path / "sensor_data2.lcm"

        for log in [log1, log2, log3]:
            shutil.copy(LCMLOG_PATH, log)

        # Test 1: Read with custom file pattern
        result = read_logs(str(tmpdir_path), file_pattern="sensor_*.lcm", verbose=False)

        # Should successfully read matching files
        assert hasattr(result, "SIM_MOLA_SEN_IMU")

        # Test 2: Read with ignore pattern
        result_ignore = read_logs(
            str(tmpdir_path),
            file_pattern="*.lcm",
            ignore_pattern=".*debug.*",
            verbose=False,
        )

        # Should successfully read non-debug files
        assert hasattr(result_ignore, "SIM_MOLA_SEN_IMU")

        # Clean up
        for pkl_file in tmpdir_path.glob("*.pkl"):
            pkl_file.unlink()
        for lcm_file in tmpdir_path.glob("*.lcm"):
            if lcm_file not in [log1, log2, log3]:
                lcm_file.unlink()


def test_read_logs_directory_not_found():
    """
    Test for the read_logs function with non-existent directory.
    """
    import pytest

    from navlib.lcmlog.parse_lcmlog import read_logs

    # Test with non-existent directory
    with pytest.raises(FileNotFoundError):
        read_logs("/nonexistent/directory")
