import json
import math
import os

import pytest
from pymseed import MS3Record, DataEncoding, MiniSEEDError

test_dir = os.path.abspath(os.path.dirname(__file__))
test_pack3 = os.path.join(test_dir, "data", "packtest_sine500.mseed3")
test_pack2 = os.path.join(test_dir, "data", "packtest_sine500.mseed2")
test_repack2_input = os.path.join(test_dir, "data", "testdata-COLA-signal.mseed2")
test_repack3_output = os.path.join(test_dir, "data", "testdata-COLA-signal.mseed3")

# A sine wave of 500 samples
sine_500 = [int(math.sin(math.radians(x)) * 500) for x in range(0, 500)]

# A global record buffer
record_buffer = b""


def record_handler(record, handler_data):
    """A callback function for MS3Record.set_record_handler()
    Stores the record in a global buffer for testing
    """
    print(f"Record handler called, record length: {len(record)}")
    global record_buffer
    record_buffer = bytes(record)


def test_msrecord_setters():
    """Test the setters for an MS3Record object."""

    # Test populating an MS3Record object with setters
    msr = MS3Record()
    msr.sourceid = "FDSN:XX_TEST__B_S_X"
    msr.reclen = 512
    msr.formatversion = 3
    msr.flags = 0x04  # Set the 4th bit (clock locked) to 1
    msr.set_starttime_str("2023-01-02T01:02:03.123456789Z")
    msr.samprate = 50.0
    msr.encoding = DataEncoding.STEIM2
    msr.pubversion = 1
    msr.extra = json.dumps({"FDSN": {"Time": {"Quality": 80}}})

    assert msr.reclen == 512
    assert msr.sourceid == "FDSN:XX_TEST__B_S_X"
    assert msr.formatversion == 3
    assert msr.flags_dict() == {"clock_locked": True}
    assert msr.starttime == 1672621323123456789
    assert msr.starttime_seconds == 1672621323.1234567
    assert msr.samprate == 50.0
    assert msr.encoding == DataEncoding.STEIM2
    assert msr.pubversion == 1
    assert msr.extra == '{"FDSN":{"Time":{"Quality":80}}}'

    # Test nanosecond starttime setter
    msr.starttime = 1672621323999999999
    assert msr.starttime == 1672621323999999999

    # Test setter for starttime_seconds, rounding UP to microsecond precision
    msr.starttime_seconds = 1672621323.123456789
    assert msr.starttime == 1672621323123457000
    assert msr.starttime_seconds == 1672621323.123457

    # Test setter for starttime_seconds, rounding DOWN to microsecond precision
    msr.starttime_seconds = 1672621323.987654321
    assert msr.starttime == 1672621323987654000
    assert msr.starttime_seconds == 1672621323.987654

def test_msrecord_extra_header():
    """Test the extra header functions for an MS3Record object."""

    # Test populating an MS3Record object with extra headers
    msr = MS3Record()
    msr.extra = """{
                   "FDSN": {
                       "Time": {
                       "Quality": 100,
                       "Correction": 1.234
                       },
                       "Flags": {
                           "MassPositionOffscale": true
                       }
                   },
                   "Operator": {
                        "Battery": {
                            "Status": "CHARGING"
                        }
                   }
                }"""

    assert msr.get_extra_header("/FDSN/Time/Quality") == 100
    assert msr.get_extra_header("/FDSN/Time/Correction") == 1.234
    assert msr.get_extra_header("/Operator/Battery/Status") == "CHARGING"
    assert msr.get_extra_header("/FDSN/Flags/MassPositionOffscale") == True

    assert msr.get_extra_header("/Nonexistent/Header") == None

    # Malformed JSON Pointer
    with pytest.raises(ValueError):
        msr.get_extra_header("Invalid/JSON/Pointer")

    # Setting existing headers
    msr.set_extra_header("/FDSN/Time/Quality", 90)
    assert msr.get_extra_header("/FDSN/Time/Quality") == 90
    msr.set_extra_header("/FDSN/Flags/MassPositionOffscale", False)
    assert msr.get_extra_header("/FDSN/Flags/MassPositionOffscale") == False
    msr.set_extra_header("/FDSN/Time/Correction", 4.321)
    assert msr.get_extra_header("/FDSN/Time/Correction") == 4.321
    msr.set_extra_header("/Operator/Battery/Status", "DISCHARGING")
    assert msr.get_extra_header("/Operator/Battery/Status") == "DISCHARGING"

    # Setting a new header
    msr.set_extra_header("/New/Header/String", "Value")
    assert msr.get_extra_header("/New/Header/String") == "Value"
    msr.set_extra_header("/New/Header/Integer", 123)
    assert msr.get_extra_header("/New/Header/Integer") == 123
    msr.set_extra_header("/New/Header/Float", 1.234)
    assert msr.get_extra_header("/New/Header/Float") == 1.234
    msr.set_extra_header("/New/Header/Boolean", True)
    assert msr.get_extra_header("/New/Header/Boolean") == True

    # Malformed JSON Pointer
    with pytest.raises(ValueError):
        msr.set_extra_header("Invalid/JSON/Pointer", "Value")

    # Test merging, replacing the existing value
    msr.merge_extra_headers('{"FDSN": {"Time": {"Quality": 80}}}')
    assert msr.get_extra_header("/FDSN/Time/Quality") == 80

    # Test merging, remove the existing value
    msr.merge_extra_headers('{"FDSN": {"Time": {"Quality": null}}}')
    assert msr.get_extra_header("/FDSN/Time/Quality") == None

    # Test merging, add a new value
    msr.merge_extra_headers('{"New": {"Header2": "Value2"}}')
    assert msr.get_extra_header("/New/Header2") == "Value2"

    # Malformed JSON Merge Patch
    with pytest.raises(ValueError):
        msr.merge_extra_headers("Invalid/JSON/Merge/Patch")


class TestMS3RecordSorting:
    """Test sorting of MS3Record objects."""
    def test_same_time_different_subsource(self):
        msr1 = MS3Record()
        msr1.set_starttime_str("2023-01-02T01:02:03.123456789Z")
        msr1.sourceid = "FDSN:XX_TEST__B_S_X"

        msr2 = MS3Record()
        msr2.set_starttime_str("2023-01-02T01:02:03.123456789Z")
        msr2.sourceid = "FDSN:XX_TEST__B_S_Y"

        assert msr1 < msr2, "Less than: Same time but different sourceid (subsource)"
        assert msr1 <= msr2, "Less than: Same time but different sourceid (subsource)"
        assert msr2 > msr1, "Less than: Same time but different sourceid (subsource)"
        assert msr2 >= msr1, "Less than: Same time but different sourceid (subsource)"

    def test_different_time_same_sourceid(self):
        msr1 = MS3Record()
        msr1.set_starttime_str("2023-01-02T01:02:03.123456789Z")
        msr1.sourceid = "FDSN:XX_TEST__B_S_X"

        msr2 = MS3Record()
        msr2.set_starttime_str("2023-01-02T01:02:04.123456789Z")  # 1 second later
        msr2.sourceid = "FDSN:XX_TEST__B_S_X"

        assert msr1 < msr2, "Less than: Different time but same sourceid"
        assert msr1 <= msr2, "Less than equal: Different time but same sourceid"
        assert msr2 > msr1, "Less than: Different time but same sourceid"
        assert msr2 >= msr1, "Less than: Different time but same sourceid"

    def test_empty_location_last(self):
        msr1 = MS3Record()
        msr1.set_starttime_str("2023-01-02T01:02:03.123456789Z")
        msr1.sourceid = "FDSN:XX_TEST_00_B_S_X"

        msr2 = MS3Record()
        msr2.set_starttime_str("2023-01-02T01:02:03.123456789Z")
        msr2.sourceid = "FDSN:XX_TEST_ZZ_B_S_Y"

        msr3 = MS3Record()
        msr3.set_starttime_str("2023-01-02T01:02:03.123456789Z")
        msr3.sourceid = "FDSN:XX_TEST__B_S_Y"

        assert (
            msr1 < msr2 < msr3
        ), "Less than: Same time but different sourceid (location)"
        assert (
            msr1 <= msr2 <= msr3
        ), "Less than equal: Same time but different sourceid (location)"
        assert (
            msr3 > msr2 > msr1
        ), "Greater than: Same time but different sourceid (location)"
        assert (
            msr3 >= msr2 >= msr1
        ), "Greater than equal: Same time but different sourceid (location)"


@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_msrecord_pack():

    msr = MS3Record()
    msr.sourceid = "FDSN:XX_TEST__B_S_X"
    msr.reclen = 512
    msr.formatversion = 3
    msr.flags = 0x04  # Set the 4th bit (clock locked) to 1
    msr.set_starttime_str("2023-01-02T01:02:03.123456789Z")
    msr.samprate = 50.0
    msr.encoding = DataEncoding.STEIM2
    msr.pubversion = 1
    msr.extra = json.dumps({"FDSN": {"Time": {"Quality": 80}}})

    # Test packing of an miniSEED v3 record
    (packed_samples, packed_records) = msr.pack(
        record_handler, data_samples=sine_500, sample_type="i"
    )

    assert packed_samples == 500
    assert packed_records == 1
    assert len(record_buffer) == 475

    with open(test_pack3, "rb") as f:
        record_v3 = f.read()
        assert record_buffer == record_v3

    # Test packing of an miniSEED v2 record
    msr.formatversion = 2

    (packed_samples, packed_records) = msr.pack(
        record_handler, data_samples=sine_500, sample_type="i"
    )

    assert packed_samples == 500
    assert packed_records == 1
    assert len(record_buffer) == 512

    with open(test_pack2, "rb") as f:
        record_v2 = f.read()
        assert record_buffer == record_v2


def test_msrecord_generate():
    """Test creating miniSEED with MS3Record.generate() method."""

    msr = MS3Record()
    msr.sourceid = "FDSN:XX_TEST__B_S_X"
    msr.reclen = 512
    msr.formatversion = 3
    msr.flags = 0x04  # Set the 4th bit (clock locked) to 1
    msr.set_starttime_str("2023-01-02T01:02:03.123456789Z")
    msr.samprate = 50.0
    msr.encoding = DataEncoding.STEIM2
    msr.pubversion = 1
    msr.extra = json.dumps({"FDSN": {"Time": {"Quality": 80}}})

    # Test creation of a miniSEED v3 record
    record_buffer = b""
    for record in msr.generate(data_samples=sine_500, sample_type="i"):
        record_buffer += record

    assert len(record_buffer) == 475

    with open(test_pack3, "rb") as f:
        record_v3 = f.read()
        assert record_buffer == record_v3

    # Test packing of an miniSEED v2 record
    msr.formatversion = 2

    record_buffer = b""
    for record in msr.generate(data_samples=sine_500, sample_type="i"):
        record_buffer += record

    assert len(record_buffer) == 512

    with open(test_pack2, "rb") as f:
        record_v2 = f.read()
        assert record_buffer == record_v2

def test_msrecord_regenerate():
    """Repack miniSEED v2 to v3 and compare to reference file."""

    record_buffer = b""

    with MS3Record.from_file(test_repack2_input, unpack_data=True) as msreader:
        for msr in msreader:

            # Set to format version 3
            msr.formatversion = 3

            # Set record length to 1024 to allow each 512-byte input record to
            # be regenerated in a single record that may be larger than the
            # input record length.
            msr.reclen = 1024

            # Regenerate the record
            for record in msr.generate():
                record_buffer += record

    assert len(record_buffer) == 617142

    with open(test_repack3_output, "rb") as f:
        record_v3 = f.read()
        assert record_buffer == record_v3


def test_msrecord_to_file(tmp_path):
    """Test MS3Record.to_file() method using pytest's tmp_path fixture."""
    # Create a new MS3Record object
    msr = MS3Record()
    msr.sourceid = "FDSN:XX_TEST__B_S_X"
    msr.reclen = 512
    msr.formatversion = 3
    msr.flags = 0x04  # Set the 4th bit (clock locked) to 1
    msr.set_starttime_str("2023-01-02T01:02:03.123456789Z")
    msr.samprate = 50.0
    msr.encoding = DataEncoding.STEIM2
    msr.pubversion = 1
    msr.extra = json.dumps({"FDSN": {"Time": {"Quality": 80}}})

    # Use pytest's tmp_path fixture to create a temporary file
    temp_file = tmp_path / "test_output.mseed"

    with msr.with_datasamples(sine_500, "i"):
        # Write using to_file method
        records_written = msr.to_file(str(temp_file), overwrite=True)

    # Verify number of records written
    assert records_written == 1

    # Verify file was created and has content
    assert temp_file.exists()
    assert temp_file.stat().st_size > 0

    # Compare created file to reference file
    with open(test_pack3, "rb") as f:
        reference_data = f.read()
        with open(temp_file, "rb") as f:
            test_data = f.read()
            assert reference_data == test_data


class TestMS3RecordParse:
    """Tests for MS3Record.parse() — single-record buffer parsing."""

    def test_parse_v3_metadata(self):
        """Parse a v3 record and verify all header fields."""
        with open(test_pack3, "rb") as f:
            buf = f.read()

        msr = MS3Record.parse(buf)

        assert msr.formatversion == 3
        assert msr.sourceid == "FDSN:XX_TEST__B_S_X"
        assert msr.samprate == 50.0
        assert msr.encoding == DataEncoding.STEIM2
        assert msr.pubversion == 1
        assert msr.samplecnt == 500
        assert msr.reclen == 475

    def test_parse_v2_metadata(self):
        """Parse a v2 record and verify all header fields."""
        with open(test_pack2, "rb") as f:
            buf = f.read()

        msr = MS3Record.parse(buf)

        assert msr.formatversion == 2
        assert msr.sourceid == "FDSN:XX_TEST__B_S_X"
        assert msr.samprate == 50.0
        assert msr.encoding == DataEncoding.STEIM2
        assert msr.samplecnt == 500
        assert msr.reclen == 512

    def test_parse_header_only(self):
        """Parse without unpacking data — header fields accessible, samples not."""
        with open(test_pack3, "rb") as f:
            buf = f.read()

        msr = MS3Record.parse(buf, unpack_data=False)

        assert msr.sourceid == "FDSN:XX_TEST__B_S_X"
        assert msr.samplecnt == 500
        assert msr.numsamples == 0

    def test_parse_with_data(self):
        """Parse with unpack_data=True — samples decoded and accessible."""
        with open(test_pack3, "rb") as f:
            buf = f.read()

        msr = MS3Record.parse(buf, unpack_data=True)

        assert msr.numsamples == 500
        assert msr.sampletype == "i"
        samples = list(msr.datasamples)
        assert samples == sine_500

    def test_parse_ownership(self):
        """Returned MS3Record owns its C struct — valid after parse() returns."""
        with open(test_pack3, "rb") as f:
            buf = f.read()

        msr = MS3Record.parse(buf, unpack_data=True)
        buf = None  # drop the input buffer; msr must remain valid

        assert msr.sourceid == "FDSN:XX_TEST__B_S_X"
        assert msr.numsamples == 500
        assert msr.samplecnt == 500
        assert list(msr.datasamples) == sine_500

    def test_parse_v2_to_v3_repack(self):
        """Parse v2 with data, switch formatversion, generate() produces a valid v3 record."""
        with open(test_pack2, "rb") as f:
            v2_buf = f.read()

        msr = MS3Record.parse(v2_buf, unpack_data=True)
        assert msr.formatversion == 2

        original_samples = list(msr.datasamples)
        original_sourceid = msr.sourceid
        original_samprate = msr.samprate
        original_samplecnt = msr.samplecnt

        msr.formatversion = 3

        output = b"".join(msr.generate())

        # Re-parse the output to verify it is a valid v3 record with the same data
        reparsed = MS3Record.parse(output, unpack_data=True)
        assert reparsed.formatversion == 3
        assert reparsed.sourceid == original_sourceid
        assert reparsed.samprate == original_samprate
        assert reparsed.samplecnt == original_samplecnt
        assert list(reparsed.datasamples) == original_samples

    def test_parse_error_truncated_buffer(self):
        """Raise MiniSEEDError when buffer is too small to contain a record."""
        with open(test_pack3, "rb") as f:
            buf = f.read()

        with pytest.raises(MiniSEEDError):
            MS3Record.parse(buf[:10])

    def test_parse_error_empty_buffer(self):
        """Raise MiniSEEDError on empty buffer."""
        with pytest.raises(MiniSEEDError):
            MS3Record.parse(b"")
