'use strict';

// Offline-cause / incident-log unit tests. Two layers, no socket server needed:
//   1. The pure classifier (lib/incident-classify) — the actual rules the live
//      device:connectivity-report + disconnect handlers apply. Testing the extracted
//      helper guarantees the handler and these assertions agree on the exact strings.
//   2. A tiny in-memory better-sqlite3 exercising the same INSERT/UPDATE the handlers
//      run, driven by the classifier's output, so the persistence shape is proven too
//      (a device:event row lands; a connectivity-report upgrades the recent offline row).

const { test } = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');

const {
  ALLOWED_EVENT_TYPES,
  isAllowedEventType,
  normalizeDisconnectReason,
  classifyConnectivity,
} = require('../lib/incident-classify');

// ---- 1. classifyConnectivity: reason/detail composition ----

test('connectivity: cold_start wins -> reason reboot', () => {
  const c = classifyConnectivity({ cold_start: true, link_lost: true });
  assert.equal(c.reason, 'reboot');
  assert.equal(c.type, 'reboot');
  assert.equal(c.detail, 'Device restarted (power/reboot)');
});

test('connectivity: link_lost true -> reason network, link-lost detail', () => {
  const c = classifyConnectivity({ link_lost: true });
  assert.equal(c.reason, 'network');
  assert.equal(c.type, 'network');
  assert.match(c.detail, /link lost/);
});

test('connectivity: link_lost false, no probe -> reason network, router/upstream detail', () => {
  const c = classifyConnectivity({ link_lost: false });
  assert.equal(c.reason, 'network');
  assert.equal(c.type, 'network');
  assert.match(c.detail, /server unreachable \(router\/internet\/upstream\)/);
});

test('connectivity: link up + internet_ok true -> server_down (OUR server, not the site)', () => {
  const c = classifyConnectivity({ link_lost: false, internet_ok: true });
  assert.equal(c.reason, 'server_down');
  assert.equal(c.type, 'network');
  assert.match(c.detail, /Internet reachable but the ScreenTinker server was unreachable/);
});

test('connectivity: link up + internet_ok false -> no_internet (router/ISP down)', () => {
  const c = classifyConnectivity({ link_lost: false, internet_ok: false });
  assert.equal(c.reason, 'no_internet');
  assert.match(c.detail, /No internet — router\/ISP down/);
});

test('connectivity: link_lost true wins over internet_ok (device link is the root cause)', () => {
  const c = classifyConnectivity({ link_lost: true, internet_ok: false });
  assert.match(c.detail, /Wi‑Fi\/Ethernet link lost/);
});

test('connectivity: ssid / weak-rssi / ip_changed fragments append to detail', () => {
  const c = classifyConnectivity({ link_lost: true, ssid: 'Office', rssi: -82, ip_changed: true });
  assert.match(c.detail, /SSID "Office"/);
  assert.match(c.detail, /weak signal \(-82 dBm\)/);
  assert.match(c.detail, /IP changed \(DHCP\/router\)/);
  // strong signal is NOT flagged
  assert.ok(!/weak signal/.test(classifyConnectivity({ link_lost: true, rssi: -50 }).detail));
});

// ---- 2. normalizeDisconnectReason: socket.io reason -> category token ----

test('disconnect reason normalizes (whitespace->_, lowercase) and defaults to silent', () => {
  assert.equal(normalizeDisconnectReason('transport close'), 'transport_close');
  assert.equal(normalizeDisconnectReason('ping timeout'), 'ping_timeout');
  assert.equal(normalizeDisconnectReason('Transport Error'), 'transport_error');
  assert.equal(normalizeDisconnectReason(''), 'silent');
  assert.equal(normalizeDisconnectReason(undefined), 'silent');
  assert.equal(normalizeDisconnectReason(null), 'silent');
});

// ---- 3. allowed event types ----

test('event types: allowed set gates device:event', () => {
  for (const t of ['offline', 'display_off', 'display_on', 'crash', 'reboot', 'network', 'app_error']) {
    assert.ok(isAllowedEventType(t), `${t} allowed`);
    assert.ok(ALLOWED_EVENT_TYPES.has(t));
  }
  assert.ok(!isAllowedEventType('bogus'));
  assert.ok(!isAllowedEventType(''));
  assert.ok(!isAllowedEventType(undefined));
});

// ---- 4. persistence: the SQL the handlers run, driven by the classifier ----

function freshDb() {
  const db = new Database(':memory:');
  db.exec(`
    CREATE TABLE device_events (
      id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, type TEXT NOT NULL,
      reason TEXT, detail TEXT, timestamp INTEGER NOT NULL DEFAULT (strftime('%s','now')));
    CREATE TABLE device_status_log (
      id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT, status TEXT, reason TEXT, detail TEXT,
      timestamp INTEGER NOT NULL DEFAULT (strftime('%s','now')));
  `);
  return db;
}

test('device:event inserts a device_events row (allowed type)', () => {
  const db = freshDb();
  // mirrors the handler body after the isAllowedEventType gate
  db.prepare("INSERT INTO device_events (device_id, type, reason, detail) VALUES (?, ?, ?, ?)")
    .run('dev1', 'display_off', null, 'screen slept');
  const rows = db.prepare('SELECT * FROM device_events WHERE device_id = ?').all('dev1');
  assert.equal(rows.length, 1);
  assert.equal(rows[0].type, 'display_off');
  assert.equal(rows[0].detail, 'screen slept');
});

test('connectivity-report upgrades the recent offline status-log row + logs an event', () => {
  const db = freshDb();
  // A server-guessed offline row exists (the disconnect handler wrote 'transport_close').
  db.prepare("INSERT INTO device_status_log (device_id, status, reason, detail) VALUES ('dev1','offline','transport_close',NULL)").run();

  // Handler path: classify the device's report, then UPDATE the recent offline row + INSERT an event.
  const { reason, detail, type } = classifyConnectivity({ link_lost: true, ssid: 'Shop', rssi: -80 });
  db.prepare(`UPDATE device_status_log SET reason = ?, detail = ?
    WHERE id = (SELECT id FROM device_status_log
      WHERE device_id = ? AND status IN ('offline','offline_timeout')
        AND timestamp > strftime('%s','now') - 900
      ORDER BY timestamp DESC, id DESC LIMIT 1)`).run(reason, detail, 'dev1');
  db.prepare("INSERT INTO device_events (device_id, type, reason, detail) VALUES (?, ?, ?, ?)")
    .run('dev1', type, reason, detail);

  const log = db.prepare("SELECT reason, detail FROM device_status_log WHERE device_id = 'dev1'").get();
  assert.equal(log.reason, 'network', 'server guess upgraded to device ground truth');
  assert.match(log.detail, /link lost/);
  assert.match(log.detail, /SSID "Shop"/);

  const ev = db.prepare("SELECT type, reason FROM device_events WHERE device_id = 'dev1'").get();
  assert.equal(ev.type, 'network');
  assert.equal(ev.reason, 'network');
});

test('connectivity-report with cold_start records a reboot event', () => {
  const db = freshDb();
  const { reason, type, detail } = classifyConnectivity({ cold_start: true });
  db.prepare("INSERT INTO device_events (device_id, type, reason, detail) VALUES (?, ?, ?, ?)")
    .run('dev2', type, reason, detail);
  const ev = db.prepare("SELECT type, reason FROM device_events WHERE device_id = 'dev2'").get();
  assert.equal(ev.type, 'reboot');
  assert.equal(ev.reason, 'reboot');
});
