"""Integration test for sgnl-inspiral with use_gstlal_cpu_upsample=True.

Identical to test_inspiral_offline but with gstlal C upsampling enabled.
"""

import sqlite3

import pytest

from sgnl.bin import inspiral

# Skip this entire module if sgnl_test_data is not installed
pytest.importorskip("sgnl_test_data")

from sgnl_test_data import get_data_path  # noqa: E402

GPS_START = 1241740448
GPS_END = 1241740960
GPS_DUR = GPS_END - GPS_START

FRAME_SPECS = [
    {"site": "H", "type": "H1_HOFT_C01"},
    {"site": "L", "type": "L1_HOFT_C01"},
    {"site": "V", "type": "V1Online"},
]

FLOAT_TOLERANCE = 1e-5

_AUTO_ID_COLUMNS = {
    "__process_id",
    "__trigger_id",
    "__event_id",
    "__trigger_map_id",
    "__param_id",
    "__segment_id",
    "_simulation_id",
}

_SKIP_TABLES = {"process", "_stillsuit_metadata"}

_TABLE_SORT_KEYS = {
    "process_params": "param",
    "filter": "_filter_id",
    "segment": "ifo, start_time",
    "simulation": "_simulation_id",
    "event": "time, network_snr",
    "trigger": "ifo, time, snr",
    "trigger_map": "__event_id, __trigger_id",
}


def _write_frame_cache(frame_dir, output_path):
    """Generate frame_local.cache with absolute paths to the trimmed frames."""
    lines = []
    for spec in FRAME_SPECS:
        fname = f"{spec['site']}-{spec['type']}-{GPS_START}-{GPS_DUR}.gwf"
        url = f"file://localhost{frame_dir / fname}"
        lines.append(f"{spec['site']} {spec['type']} {GPS_START} {GPS_DUR} {url}")
    output_path.write_text("\n".join(lines) + "\n")


def _compare_databases(ref_path, test_path):
    """Compare all tables between reference and test sqlite files.

    Returns a list of error messages (empty if databases match).
    """
    ref = sqlite3.connect(ref_path)
    test = sqlite3.connect(test_path)
    errors = []

    ref_tables = {
        r[0]
        for r in ref.execute(
            "SELECT name FROM sqlite_master WHERE type='table'"
        ).fetchall()
    }
    test_tables = {
        r[0]
        for r in test.execute(
            "SELECT name FROM sqlite_master WHERE type='table'"
        ).fetchall()
    }

    ref_compare = ref_tables - _SKIP_TABLES
    test_compare = test_tables - _SKIP_TABLES
    if ref_compare != test_compare:
        only_ref = ref_compare - test_compare
        only_test = test_compare - ref_compare
        if only_ref:
            errors.append(f"Tables only in ref: {only_ref}")
        if only_test:
            errors.append(f"Tables only in test: {only_test}")

    for table in sorted(ref_compare & test_compare):
        cols_info = ref.execute(f"PRAGMA table_info({table})").fetchall()
        col_names = [c[1] for c in cols_info]
        col_types = {c[1]: c[2] for c in cols_info}

        compare_cols = [c for c in col_names if c not in _AUTO_ID_COLUMNS]
        if not compare_cols:
            continue

        sort_key = _TABLE_SORT_KEYS.get(table, compare_cols[0])
        cols_str = ", ".join(compare_cols)

        query = f"SELECT {cols_str} FROM {table} ORDER BY {sort_key}"  # noqa: S608
        ref_rows = ref.execute(query).fetchall()
        test_rows = test.execute(query).fetchall()

        if len(ref_rows) != len(test_rows):
            errors.append(
                f"{table}: row count mismatch ref={len(ref_rows)} "
                f"test={len(test_rows)}"
            )
            continue

        for i, (rrow, trow) in enumerate(zip(ref_rows, test_rows)):
            for j, col in enumerate(compare_cols):
                rv, tv = rrow[j], trow[j]
                if rv is None and tv is None:
                    continue
                if col_types[col] == "REAL":
                    if rv is None or tv is None or abs(rv - tv) > FLOAT_TOLERANCE:
                        errors.append(
                            f"{table} row {i}: {col} mismatch " f"ref={rv} test={tv}"
                        )
                else:
                    if rv != tv:
                        errors.append(
                            f"{table} row {i}: {col} mismatch " f"ref={rv} test={tv}"
                        )

    # --- Verify trigger-event mapping consistency ---
    ref_events = ref.execute(
        "SELECT __event_id, time, network_snr FROM event " "ORDER BY time, network_snr"
    ).fetchall()
    test_events = test.execute(
        "SELECT __event_id, time, network_snr FROM event " "ORDER BY time, network_snr"
    ).fetchall()

    if len(ref_events) == len(test_events):
        for i, (rev, tev) in enumerate(zip(ref_events, test_events)):
            ref_trigs = ref.execute(
                "SELECT t.ifo, t.time, t.snr FROM trigger t "
                "JOIN trigger_map tm ON tm.__trigger_id = t.__trigger_id "
                "WHERE tm.__event_id = ? ORDER BY t.ifo, t.time",
                (rev[0],),
            ).fetchall()
            test_trigs = test.execute(
                "SELECT t.ifo, t.time, t.snr FROM trigger t "
                "JOIN trigger_map tm ON tm.__trigger_id = t.__trigger_id "
                "WHERE tm.__event_id = ? ORDER BY t.ifo, t.time",
                (tev[0],),
            ).fetchall()
            if len(ref_trigs) != len(test_trigs):
                errors.append(
                    f"trigger_map event {i} (time={rev[1]}): "
                    f"trigger count ref={len(ref_trigs)} test={len(test_trigs)}"
                )
            else:
                for rt, tt in zip(ref_trigs, test_trigs):
                    if rt[0] != tt[0] or rt[1] != tt[1]:
                        errors.append(
                            f"trigger_map event {i} (time={rev[1]}): "
                            f"ifo/time mismatch ref={rt[:2]} test={tt[:2]}"
                        )
                        break
                    if abs(rt[2] - tt[2]) > FLOAT_TOLERANCE:
                        errors.append(
                            f"trigger_map event {i} (time={rev[1]}): "
                            f"snr mismatch ref={rt[2]} test={tt[2]}"
                        )
                        break

    ref.close()
    test.close()
    return errors


class TestInspiralOfflineGstlalUpsample:
    """Integration test for offline inspiral with gstlal C upsampling."""

    def test_inspiral_frames_gstlal_upsample(self, tmp_path):
        """Run sgnl-inspiral with use_gstlal_cpu_upsample=True."""
        from sgnligo.sources import DataSourceInfo
        from sgnligo.transforms import ConditionInfo

        offline_dir = get_data_path("offline")
        svd_bank_dir = get_data_path("online/svd_bank")

        frame_cache = tmp_path / "frame_local.cache"
        _write_frame_cache(offline_dir, frame_cache)

        trigger_output = str(tmp_path / "test_triggers.sqlite")

        data_source_info = DataSourceInfo(
            data_source="frames",
            channel_name=[
                "H1=DCS-CALIB_STRAIN_CLEAN_C01",
                "L1=DCS-CALIB_STRAIN_CLEAN_C01",
                "V1=Hrec_hoft_16384Hz",
            ],
            gps_start_time=GPS_START,
            gps_end_time=GPS_END,
            frame_cache=str(frame_cache),
            frame_segments_file=str(offline_dir / "segments.xml.gz"),
            frame_segments_name="datasegments",
        )

        condition_info = ConditionInfo(
            psd_fft_length=8,
            reference_psd=(
                offline_dir / "H1L1V1-SGNL_REFERENCE_PSD-1241738590-3835.xml.gz"
            ).as_posix(),
            ht_gate_threshold=50.0,
            track_psd=True,
        )

        inspiral.inspiral(
            data_source_info=data_source_info,
            condition_info=condition_info,
            svd_bank=[
                str(svd_bank_dir / "H1-0000_GSTLAL_SVD_BANK-1241725020-760106.xml.gz"),
                str(svd_bank_dir / "L1-0000_GSTLAL_SVD_BANK-1241725020-760106.xml.gz"),
                str(svd_bank_dir / "V1-0000_GSTLAL_SVD_BANK-1241725020-760106.xml.gz"),
            ],
            trigger_output=[trigger_output],
            event_config=str(offline_dir / "fake_cbc.yaml"),
            coincidence_threshold=0.01,
            snr_min=4,
            min_instruments_candidates=1,
            torch_device="cpu",
            torch_dtype="float32",
            trigger_finding_duration=1,
            use_gstlal_cpu_upsample=True,
        )

        # Compare all tables against reference
        ref_path = str(offline_dir / "reference_triggers.sqlite")
        errors = _compare_databases(ref_path, trigger_output)
        assert not errors, f"{len(errors)} mismatches found:\n" + "\n".join(errors)
