import pathlib

import pytest
from sgn.apps import Pipeline
from sgn.sinks import NullSink
from sgnts.sinks import DumpSeriesSink
from sgnts.transforms import Resampler

from sgn_gwframe.sources import GWFrameSource

PATH_DATA = pathlib.Path(__file__).parent / "data"


def test_framesource(tmp_path):
    gps_start_time = 1240215487
    gps_end_time = 1240215519
    frame_cache = PATH_DATA / "gw190425.cache"
    channels = ["L1:GWOSC-16KHZ_R1_STRAIN", "L1:GWOSC-16KHZ_R1_DQMASK"]
    instrument = "L1"

    pipeline = Pipeline()

    #
    #       ----------
    #      | src1     |
    #       ----------
    #       /      \
    #   DQ /        \ Strain
    #  ------   ------------
    # | Null | | Resampler  |
    #  ------   ------------
    #                 \
    #                  \
    #             ---------
    #            | snk1    |
    #             ---------

    src = GWFrameSource(
        name="src1",
        frames=frame_cache.as_posix(),
        channels=channels,
        instrument=instrument,
        start=gps_start_time,
        end=gps_end_time,
    )
    sample_rate = src.rates[channels[0]]
    strain_file = tmp_path / "strain.txt"
    pipeline.insert(
        src,
        Resampler(
            name="trans1",
            source_pad_names=(instrument,),
            sink_pad_names=(instrument,),
            inrate=sample_rate,
            outrate=2048,
        ),
        DumpSeriesSink(
            name="snk1", sink_pad_names=(instrument,), fname=str(strain_file)
        ),
        NullSink(name="snk2", sink_pad_names=("DQ",)),
    )

    pipeline.insert(
        link_map={
            "trans1:snk:" + instrument: "src1:src:L1:GWOSC-16KHZ_R1_STRAIN",
            "snk1:snk:" + instrument: "trans1:src:" + instrument,
            "snk2:snk:DQ": "src1:src:L1:GWOSC-16KHZ_R1_DQMASK",
        }
    )

    pipeline.run()


def test_framesource_requires_start_time():
    """Test that GWFrameSource raises ValueError when start is None."""
    frame_cache = PATH_DATA / "gw190425.cache"
    channels = ["L1:GWOSC-16KHZ_R1_STRAIN"]

    with pytest.raises(ValueError, match="requires a start time"):
        GWFrameSource(
            name="src",
            frames=frame_cache.as_posix(),
            channels=channels,
            start=None,
            end=1240215519,
        )


def test_framesource_instrument_mismatch():
    """Test that source raises ValueError when instrument doesn't match channel."""
    frame_cache = PATH_DATA / "gw190425.cache"
    channels = ["L1:GWOSC-16KHZ_R1_STRAIN"]

    with pytest.raises(ValueError, match="Instrument 'H1' does not match"):
        GWFrameSource(
            name="src",
            frames=frame_cache.as_posix(),
            channels=channels,
            instrument="H1",  # Mismatch: channel is L1, instrument is H1
            start=1240215487,
            end=1240215519,
        )


def test_framesource_malformed_cache_line(tmp_path):
    """Test that GWFrameSource raises ValueError for malformed cache lines."""
    # Create a cache file with invalid format (missing columns)
    bad_cache = tmp_path / "bad.cache"
    bad_cache.write_text("L L1_DATA 1234567890\n")  # Only 3 fields, need 5

    with pytest.raises(ValueError, match="Invalid cache line format"):
        GWFrameSource(
            name="src",
            frames=bad_cache.as_posix(),
            channels=["L1:TEST"],
            start=1234567890,
            end=1234567900,
        )


def test_framesource_nonexistent_cache_file(tmp_path):
    """Test that GWFrameSource raises FileNotFoundError for missing cache file."""
    nonexistent = tmp_path / "does_not_exist.cache"

    with pytest.raises(FileNotFoundError):
        GWFrameSource(
            name="src",
            frames=nonexistent.as_posix(),
            channels=["L1:TEST"],
            start=1234567890,
            end=1234567900,
        )


def test_framesource_empty_cache(tmp_path):
    """Test that GWFrameSource handles empty cache file."""
    empty_cache = tmp_path / "empty.cache"
    empty_cache.write_text("")

    # Should raise IndexError when trying to access cache[0]
    with pytest.raises(IndexError):
        GWFrameSource(
            name="src",
            frames=empty_cache.as_posix(),
            channels=["L1:TEST"],
            start=1234567890,
            end=1234567900,
        )


def test_framesource_no_matching_segments(tmp_path):
    """Test GWFrameSource with cache that doesn't cover requested time range."""
    # Create cache with data at GPS time 1000000000
    cache_file = tmp_path / "nomatch.cache"
    fake_gwf = tmp_path / "fake.gwf"
    fake_gwf.touch()  # Create empty file
    cache_file.write_text(f"L L1_TEST 1000000000 32 {fake_gwf}\n")

    # Request data at different time (2000000000) - no overlap
    # Should raise IndexError when cache is empty after filtering
    with pytest.raises(IndexError):
        GWFrameSource(
            name="src",
            frames=cache_file.as_posix(),
            channels=["L1:TEST"],
            start=2000000000,
            end=2000000032,
        )


def test_framesource_cache_with_gaps(tmp_path, caplog):
    """Test GWFrameSource with cache that has gaps in time coverage."""
    import gwframe
    import numpy as np

    # Create two frame files with a gap between them
    # File 1: GPS 1000000000-1000000032 (32 seconds)
    # GAP: 1000000032-1000000064 (32 seconds missing)
    # File 2: GPS 1000000064-1000000096 (32 seconds)

    file1 = tmp_path / "file1.gwf"
    file2 = tmp_path / "file2.gwf"

    # Create first frame file
    frame1 = gwframe.Frame(t0=1000000000, duration=32, name="L1")
    data1 = np.random.randn(32 * 16384)
    frame1.add_channel("L1:TEST", data1, sample_rate=16384)
    frame1.write(str(file1))

    # Create second frame file (with gap)
    frame2 = gwframe.Frame(t0=1000000064, duration=32, name="L1")
    data2 = np.random.randn(32 * 16384)
    frame2.add_channel("L1:TEST", data2, sample_rate=16384)
    frame2.write(str(file2))

    # Create cache file
    cache_file = tmp_path / "gap.cache"
    cache_file.write_text(
        f"L L1_TEST 1000000000 32 {file1}\nL L1_TEST 1000000064 32 {file2}\n"
    )

    # Create GWFrameSource - should detect missing segment and log warning
    src = GWFrameSource(
        name="src",
        frames=cache_file.as_posix(),
        channels=["L1:TEST"],
        start=1000000000,
        end=1000000096,
    )

    # Verify missing segment was detected
    assert "missing segments" in caplog.text.lower()

    # Run pipeline to trigger gap buffer generation
    pipeline = Pipeline()
    null_sink = NullSink(name="snk", sink_pad_names=("L1:TEST",))
    pipeline.connect(src, null_sink)

    # Should complete successfully despite gap (gap buffer sent)
    pipeline.run()

    # Check that gap warning was logged during data loading
    assert "unexpected epoch" in caplog.text.lower()


def test_framesource_out_of_order_data(tmp_path):
    """Test GWFrameSource raises error when cache files are out of time order."""
    import gwframe
    import numpy as np

    # Create two frame files with backward time (out of order)
    # File 1: GPS 1000000000-1000000032
    # File 2: GPS 1000000000-1000000032 (same time - would cause last_epoch > start)

    file1 = tmp_path / "file1.gwf"
    file2 = tmp_path / "file2.gwf"

    frame1 = gwframe.Frame(t0=1000000000, duration=32, name="L1")
    data1 = np.random.randn(32 * 16384)
    frame1.add_channel("L1:TEST", data1, sample_rate=16384)
    frame1.write(str(file1))

    frame2 = gwframe.Frame(t0=1000000000, duration=32, name="L1")
    data2 = np.random.randn(32 * 16384)
    frame2.add_channel("L1:TEST", data2, sample_rate=16384)
    frame2.write(str(file2))

    # Create cache with out-of-order times
    cache_file = tmp_path / "outoforder.cache"
    cache_file.write_text(
        f"L L1_TEST 1000000000 32 {file1}\nL L1_TEST 1000000000 32 {file2}\n"
    )

    src = GWFrameSource(
        name="src",
        frames=cache_file.as_posix(),
        channels=["L1:TEST"],
        start=1000000000,
        end=1000000064,
    )

    pipeline = Pipeline()
    null_sink = NullSink(name="snk", sink_pad_names=("L1:TEST",))
    pipeline.connect(src, null_sink)

    # Should raise ValueError for out-of-order data
    with pytest.raises(ValueError, match="Unepected epoch"):
        pipeline.run()


def test_framesource_complex_missing_segments(tmp_path, caplog):
    """Test GWFrameSource with multiple gaps in cache coverage."""
    import gwframe
    import numpy as np

    # Create cache with multiple gaps:
    # File 1: 1000000000-1000000032
    # GAP 1: 1000000032-1000000064
    # File 2: 1000000064-1000000096
    # GAP 2: 1000000096-1000000128
    # File 3: 1000000128-1000000160

    files = []
    times = [1000000000, 1000000064, 1000000128]

    for i, t0 in enumerate(times):
        file_path = tmp_path / f"file{i}.gwf"
        frame = gwframe.Frame(t0=t0, duration=32, name="L1")
        data = np.random.randn(32 * 16384)
        frame.add_channel("L1:TEST", data, sample_rate=16384)
        frame.write(str(file_path))
        files.append(file_path)

    # Create cache
    cache_file = tmp_path / "multiplegaps.cache"
    cache_lines = [f"L L1_TEST {t0} 32 {f}\n" for t0, f in zip(times, files)]
    cache_file.write_text("".join(cache_lines))

    # Create GWFrameSource requesting full range (including gaps)
    src = GWFrameSource(
        name="src",
        frames=cache_file.as_posix(),
        channels=["L1:TEST"],
        start=1000000000,
        end=1000000160,
    )

    # Should detect multiple missing segments
    assert "missing segments" in caplog.text.lower()

    # Verify it can still run (with gap buffers)
    pipeline = Pipeline()
    null_sink = NullSink(name="snk", sink_pad_names=("L1:TEST",))
    pipeline.connect(src, null_sink)
    pipeline.run()


def test_framesource_single_gwf_file(tmp_path):
    """Test GWFrameSource with a single .gwf file instead of cache."""
    import gwframe
    import numpy as np

    # Create a single frame file
    gwf_file = tmp_path / "test.gwf"
    frame = gwframe.Frame(t0=1000000000, duration=32, name="H1")
    data = np.random.randn(32 * 16384)
    frame.add_channel("H1:TEST", data, sample_rate=16384)
    frame.write(str(gwf_file))

    # Create GWFrameSource with single gwf file
    src = GWFrameSource(
        name="src",
        frames=str(gwf_file),
        channels=["H1:TEST"],
        start=1000000000,
        end=1000000032,
    )

    # Verify it initialized correctly
    assert "H1:TEST" in src.rates
    assert src.rates["H1:TEST"] == 16384

    # Run through pipeline
    pipeline = Pipeline()
    null_sink = NullSink(name="snk", sink_pad_names=("H1:TEST",))
    pipeline.connect(src, null_sink)
    pipeline.run()


def test_framesource_list_of_gwf_files(tmp_path):
    """Test GWFrameSource with a list of .gwf files."""
    import gwframe
    import numpy as np

    # Create multiple frame files
    files = []
    times = [1000000000, 1000000032, 1000000064]

    for i, t0 in enumerate(times):
        gwf_file = tmp_path / f"frame{i}.gwf"
        frame = gwframe.Frame(t0=t0, duration=32, name="L1")
        data = np.random.randn(32 * 16384)
        frame.add_channel("L1:TEST", data, sample_rate=16384)
        frame.write(str(gwf_file))
        files.append(str(gwf_file))

    # Create GWFrameSource with list of files
    src = GWFrameSource(
        name="src",
        frames=files,
        channels=["L1:TEST"],
        start=1000000000,
        end=1000000096,
    )

    # Verify it initialized correctly
    assert "L1:TEST" in src.rates
    assert src.rates["L1:TEST"] == 16384

    # Run through pipeline
    pipeline = Pipeline()
    null_sink = NullSink(name="snk", sink_pad_names=("L1:TEST",))
    pipeline.connect(src, null_sink)
    pipeline.run()


def test_framesource_unsupported_file_type(tmp_path):
    """Test GWFrameSource raises error for unsupported file types."""
    # Create a file with unsupported extension
    bad_file = tmp_path / "data.txt"
    bad_file.write_text("some data")

    # Should raise ValueError for unsupported file type
    with pytest.raises(
        ValueError, match=r"Unsupported file type.*Expected \.cache or \.gwf"
    ):
        GWFrameSource(
            name="src",
            frames=str(bad_file),
            channels=["H1:TEST"],
            start=1000000000,
            end=1000000032,
        )


def test_framesource_invalid_frames_type():
    """Test GWFrameSource raises TypeError for invalid frames parameter type."""
    # Should raise TypeError for non-string/list frames parameter
    with pytest.raises(TypeError, match="frames must be str or list"):
        GWFrameSource(
            name="src",
            frames=12345,  # type: ignore[arg-type]  # Invalid type - intentional for test
            channels=["H1:TEST"],
            start=1000000000,
            end=1000000032,
        )


def test_framesource_cache_extends_beyond_segment(tmp_path, caplog):
    """Test GWFrameSource when cache entry extends beyond requested segment."""
    import gwframe
    import numpy as np

    # Create a frame file that extends beyond requested segment
    # File: 1000000000-1000000064 (64 seconds)
    # But we only request: 1000000000-1000000032 (32 seconds)
    gwf_file = tmp_path / "extended.gwf"
    frame = gwframe.Frame(t0=1000000000, duration=64, name="H1")
    data = np.random.randn(64 * 16384)
    frame.add_channel("H1:TEST", data, sample_rate=16384)
    frame.write(str(gwf_file))

    # Create cache file
    cache_file = tmp_path / "extended.cache"
    cache_file.write_text(f"H H1_TEST 1000000000 64 {gwf_file}\n")

    # Create GWFrameSource requesting only first 32 seconds
    src = GWFrameSource(
        name="src",
        frames=cache_file.as_posix(),
        channels=["H1:TEST"],
        start=1000000000,
        end=1000000032,  # Cache extends to 1000000064
    )

    # Should not report missing segments (cache fully covers request)
    assert "missing segments" not in caplog.text.lower()

    # Run through pipeline
    pipeline = Pipeline()
    null_sink = NullSink(name="snk", sink_pad_names=("H1:TEST",))
    pipeline.connect(src, null_sink)
    pipeline.run()


def test_framesource_gap_with_extended_file(tmp_path, caplog):
    """Test source with gap followed by file extending beyond requested segment."""
    import gwframe
    import numpy as np

    # Create cache with gap and file that extends beyond request:
    # Request: 1000000000-1000000100
    # File 1: 1000000000-1000000032
    # GAP: 1000000032-1000000064
    # File 2: 1000000064-1000000200 (extends way beyond requested 1000000100)

    file1 = tmp_path / "file1.gwf"
    file2 = tmp_path / "file2.gwf"

    frame1 = gwframe.Frame(t0=1000000000, duration=32, name="H1")
    data1 = np.random.randn(32 * 16384)
    frame1.add_channel("H1:TEST", data1, sample_rate=16384)
    frame1.write(str(file1))

    frame2 = gwframe.Frame(t0=1000000064, duration=136, name="H1")
    data2 = np.random.randn(136 * 16384)
    frame2.add_channel("H1:TEST", data2, sample_rate=16384)
    frame2.write(str(file2))

    # Create cache
    cache_file = tmp_path / "gap_extended.cache"
    cache_file.write_text(
        f"H H1_TEST 1000000000 32 {file1}\nH H1_TEST 1000000064 136 {file2}\n"
    )

    # Request segment that has gap and ends before second file ends
    src = GWFrameSource(
        name="src",
        frames=cache_file.as_posix(),
        channels=["H1:TEST"],
        start=1000000000,
        end=1000000100,  # File 2 extends to 1000000200
    )

    # Should detect the gap
    assert "missing segments" in caplog.text.lower()

    # Run through pipeline
    pipeline = Pipeline()
    null_sink = NullSink(name="snk", sink_pad_names=("H1:TEST",))
    pipeline.connect(src, null_sink)
    pipeline.run()
