"""Tests for v0.6-1 FIRMS handler (storage-only).

The handler never returns a wire string (storage-only contract). All
assertions check side effects on `firms_pixels` + `event_log`. Per the
audit doc finding #2 the handler must close the v0.5.13 silent-drop on
`central.fire.hotspot.>` envelopes.

Envelope shape sourced from firms-investigation.md sampling (250 envelopes
2026-05-28..06-04, all VIIRS).
"""
import pytest

from meshai.central import firms_handler
from meshai.central.firms_handler import handle_firms
from meshai.persistence import close_thread_connection, init_db
from meshai.persistence import db as persistence_db


# ---------- fixtures --------------------------------------------------------


@pytest.fixture
def mem_db(monkeypatch, tmp_path):
    db_path = str(tmp_path / "firms-test.sqlite")
    monkeypatch.setenv("MESHAI_DB_PATH", db_path)
    persistence_db._initialised.clear()
    close_thread_connection()
    conn = init_db()
    yield conn
    close_thread_connection()
    persistence_db._initialised.discard(db_path)


def _firms_env(*,
               lat=42.19664, lon=-113.70981, frp=135.93,
               confidence="high", satellite="N",
               acq_date="2026-05-28", acq_time="1949",
               bright_ti4=367.0, daynight="D",
               envelope_id="firms_001",
               category="fire.hotspot.viirs",
               severity=3,
               region="unknown",
               extras=None):
    """Build a FIRMS CloudEvents envelope matching the live wire shape.

    Default fixture is SAMPLE B from firms-investigation.md (Cache Peak
    high-confidence 135 MW fire). Override individual fields for filter
    coverage.
    """
    payload = {
        "id":          envelope_id,
        "latitude":    lat,
        "longitude":   lon,
        "frp":         frp,
        "confidence":  confidence,
        "satellite":   satellite,
        "instrument":  "VIIRS",
        "acq_date":    acq_date,
        "acq_time":    acq_time,
        "bright_ti4":  bright_ti4,
        "daynight":    daynight,
        "version":     "2.0NRT",
        "_enriched":   {"geocoder": {"landclass": "Cache Peak Roadless Area",
                                       "elevation_m": 2151.9}},
    }
    if extras: payload.update(extras)
    return {
        "subject": f"central.fire.hotspot.viirs_snpp.{confidence}.{region}",
        "id":      envelope_id,
        "data": {
            "id":       envelope_id,
            "adapter":  "firms",
            "category": category,
            "severity": severity,
            "time":     "2026-05-28T19:49:00Z",
            "geo":      {"centroid": [lon, lat]},
            "data":     payload,
        },
    }


def _row_count(mem_db, table):
    return mem_db.execute(f"SELECT COUNT(*) AS n FROM {table}").fetchone()["n"]


def _last_event_log(mem_db):
    r = mem_db.execute(
        "SELECT * FROM event_log ORDER BY id DESC LIMIT 1"
    ).fetchone()
    return dict(r) if r else None


# ============================================================================
# Happy path: high-confidence pixel stored
# ============================================================================


def test_high_confidence_pixel_persisted(mem_db):
    env = _firms_env()
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)

    assert out is None, "storage-only handler must never return a wire string"
    assert _row_count(mem_db, "firms_pixels") == 1

    row = mem_db.execute("SELECT * FROM firms_pixels").fetchone()
    assert row["lat"] == pytest.approx(42.19664)
    assert row["lon"] == pytest.approx(-113.70981)
    assert row["frp"] == pytest.approx(135.93)
    assert row["confidence"] == "high"
    assert row["satellite"] == "N"
    assert row["brightness"] == pytest.approx(367.0)
    assert row["irwin_id"] is None  # unattached; v0.6 fire-tracker fills later

    # acq_time should be the parsed UTC epoch of 2026-05-28 19:49Z.
    import datetime as _dt
    expected = int(_dt.datetime(2026, 5, 28, 19, 49,
                                  tzinfo=_dt.timezone.utc).timestamp())
    assert row["acq_time"] == expected

    # event_log: handled=1, table_name set, table_pk = inserted rowid.
    log = _last_event_log(mem_db)
    assert log["source"] == "firms"
    assert log["handled"] == 1
    assert log["table_name"] == "firms_pixels"
    assert log["table_pk"] == str(row["id"])


def test_nominal_confidence_pixel_persisted_under_default_floor(mem_db):
    """Default FIRMS_CONFIDENCE_FLOOR='low' must accept nominal/high/low."""
    env = _firms_env(confidence="nominal", envelope_id="firms_002")
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 1


def test_low_confidence_pixel_persisted_under_default_floor(mem_db):
    env = _firms_env(confidence="low", envelope_id="firms_003")
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 1


# ============================================================================
# Confidence floor when bumped up
# ============================================================================


def test_low_confidence_dropped_when_floor_is_nominal(mem_db, monkeypatch):
    monkeypatch.setattr(firms_handler, "FIRMS_CONFIDENCE_FLOOR", "nominal")
    env = _firms_env(confidence="low", envelope_id="firms_lo")
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 0

    log = _last_event_log(mem_db)
    assert log["handled"] == 0
    assert log["category"].endswith("|below_confidence_floor")


def test_unknown_confidence_value_dropped(mem_db):
    env = _firms_env(confidence="bogus", envelope_id="firms_bog")
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 0
    log = _last_event_log(mem_db)
    assert log["category"].endswith("|below_confidence_floor")


# ============================================================================
# FRP floor
# ============================================================================


def test_low_frp_dropped_when_floor_set(mem_db, monkeypatch):
    monkeypatch.setattr(firms_handler, "FIRMS_FRP_FLOOR", 5.0)
    env = _firms_env(frp=1.4, confidence="high", envelope_id="firms_frp_low")
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 0
    log = _last_event_log(mem_db)
    assert log["category"].endswith("|below_frp_floor")


def test_frp_at_floor_stored(mem_db, monkeypatch):
    monkeypatch.setattr(firms_handler, "FIRMS_FRP_FLOOR", 5.0)
    env = _firms_env(frp=5.0, confidence="high", envelope_id="firms_frp_at")
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 1


def test_missing_frp_dropped_when_floor_set(mem_db, monkeypatch):
    monkeypatch.setattr(firms_handler, "FIRMS_FRP_FLOOR", 5.0)
    env = _firms_env(confidence="high", envelope_id="firms_no_frp",
                      extras={"frp": None})
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 0


def test_missing_frp_stored_when_floor_zero(mem_db):
    """Default FIRMS_FRP_FLOOR=0: missing FRP still stores (null in column)."""
    env = _firms_env(confidence="high", envelope_id="firms_no_frp_2",
                      extras={"frp": None})
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 1
    row = mem_db.execute("SELECT * FROM firms_pixels").fetchone()
    assert row["frp"] is None


# ============================================================================
# Bbox filter (default None = pass-through; explicit bbox tested both sides)
# ============================================================================


def test_bbox_none_passes_through(mem_db):
    env = _firms_env(lat=51.0, lon=10.0,   # Germany
                      envelope_id="firms_eu")
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 1


def test_bbox_drops_outside(mem_db, monkeypatch):
    # Idaho-ish bbox.
    monkeypatch.setattr(firms_handler, "FIRMS_BBOX_OPTIONAL",
                        (42.0, -117.5, 49.0, -111.0))
    env = _firms_env(lat=51.0, lon=10.0, envelope_id="firms_out")
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 0
    log = _last_event_log(mem_db)
    assert log["category"].endswith("|outside_bbox")


def test_bbox_keeps_inside(mem_db, monkeypatch):
    monkeypatch.setattr(firms_handler, "FIRMS_BBOX_OPTIONAL",
                        (42.0, -117.5, 49.0, -111.0))
    env = _firms_env(envelope_id="firms_in")   # Cache Peak is inside
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 1


# ============================================================================
# Dedup: same satellite pixel observation arriving twice = no-op
# ============================================================================


def test_dedup_same_pixel_idempotent(mem_db):
    env = _firms_env(envelope_id="dup_001")
    handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    handle_firms(env, env["subject"], data={}, now=1_780_660_001)

    assert _row_count(mem_db, "firms_pixels") == 1, "OR IGNORE collapses dup"
    # Two event_log rows; second is dedup_hit.
    rows = mem_db.execute(
        "SELECT category, table_name, handled FROM event_log "
        "WHERE source='firms' ORDER BY id"
    ).fetchall()
    assert len(rows) == 2
    assert rows[0]["table_name"] == "firms_pixels"
    assert rows[1]["table_name"] is None
    assert rows[1]["category"].endswith("|dedup_hit")


def test_dedup_collapses_lat_lon_float_noise(mem_db):
    """Same coord with sub-1m float noise must hit the same dedup key.
    5-decimal rounding => differences in the 6th+ decimal are absorbed."""
    e1 = _firms_env(lat=42.196641234567, lon=-113.709810000001,
                     envelope_id="dup_a")
    e2 = _firms_env(lat=42.196641111111, lon=-113.709810999999,
                     envelope_id="dup_b")
    handle_firms(e1, e1["subject"], data={}, now=1_780_660_000)
    handle_firms(e2, e2["subject"], data={}, now=1_780_660_001)
    assert _row_count(mem_db, "firms_pixels") == 1


def test_dedup_different_satellite_stored_separately(mem_db):
    """Same coord + acq_time but different satellite is 2 distinct observations."""
    e1 = _firms_env(satellite="N",   envelope_id="sat_n")
    e2 = _firms_env(satellite="N20", envelope_id="sat_n20")
    handle_firms(e1, e1["subject"], data={}, now=1_780_660_000)
    handle_firms(e2, e2["subject"], data={}, now=1_780_660_001)
    assert _row_count(mem_db, "firms_pixels") == 2


def test_dedup_different_acq_time_stored_separately(mem_db):
    """Same pixel observed on two passes 12h apart -> 2 rows."""
    e1 = _firms_env(acq_time="0700", envelope_id="t1")
    e2 = _firms_env(acq_time="1900", envelope_id="t2")
    handle_firms(e1, e1["subject"], data={}, now=1_780_660_000)
    handle_firms(e2, e2["subject"], data={}, now=1_780_660_001)
    assert _row_count(mem_db, "firms_pixels") == 2


# ============================================================================
# Bad / missing inputs
# ============================================================================


def test_missing_coords_dropped(mem_db):
    env = _firms_env(envelope_id="no_coords",
                      extras={"latitude": None, "longitude": None})
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 0
    log = _last_event_log(mem_db)
    assert log["category"].endswith("|missing_coords")


def test_missing_acq_time_dropped(mem_db):
    env = _firms_env(envelope_id="no_acq",
                      extras={"acq_date": None, "acq_time": None})
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 0
    log = _last_event_log(mem_db)
    assert log["category"].endswith("|missing_acq_time")


def test_non_firms_adapter_passes_through(mem_db):
    """Defense in depth: handler must early-return on non-firms envelopes."""
    env = _firms_env(envelope_id="wrong_adapter")
    env["data"]["adapter"] = "nws"
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 0
    assert _row_count(mem_db, "event_log") == 0


def test_acq_time_int_accepted(mem_db):
    """FIRMS sometimes publishes acq_time as int 2013 rather than '2013'."""
    env = _firms_env(envelope_id="int_acq",
                      extras={"acq_time": 1949})
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 1


def test_short_acq_time_zero_padded(mem_db):
    """acq_time '49' (early-morning pass) must zero-pad to '0049'."""
    env = _firms_env(envelope_id="short_acq",
                      extras={"acq_time": "49"})
    out = handle_firms(env, env["subject"], data={}, now=1_780_660_000)
    assert out is None
    assert _row_count(mem_db, "firms_pixels") == 1


# ============================================================================
# Integration: envelope through the full consumer -> handler -> SQLite path
# ============================================================================


def test_end_to_end_envelope_through_consumer(mem_db, monkeypatch):
    """Confirm: envelope enters consumer._normalize, handle_firms is invoked,
    firms_pixels row is inserted, and consumer returns None (default-deny
    keeps the broadcast suppressed). mesh_broadcasts_out MUST stay empty."""
    from unittest.mock import MagicMock
    from meshai.config import Config
    from meshai.central.consumer import CentralConsumer

    cfg = Config()
    cfg.notifications.cold_start_grace_seconds = 0
    bus = MagicMock()
    consumer = CentralConsumer(cfg.environmental, bus)

    env = _firms_env(envelope_id="e2e_001")
    out = consumer._normalize(env["subject"], env)

    # consumer.normalize returns None -> Event never reaches bus.
    assert out is None
    bus.emit.assert_not_called()

    # firms_pixels MUST have the row; mesh_broadcasts_out MUST be empty.
    assert _row_count(mem_db, "firms_pixels") == 1
    assert _row_count(mem_db, "mesh_broadcasts_out") == 0

    # event_log records the storage.
    log = _last_event_log(mem_db)
    assert log["source"] == "firms"
    assert log["handled"] == 1
    assert log["table_name"] == "firms_pixels"
