'use strict';

// Hardware identity — model, OS build, serial, which output — is reported by the panel and had
// nowhere to live: the devices row carried only `platform`. A BrightSign knows all four, and an
// operator looking at a dead screen wants the serial and the model, not a guess.
//
// The reason this is a SEPARATE writer rather than four more fields in applyDeviceInfo: that
// function is a blind full-row overwrite, and an empty device_info once nulled seventeen columns
// every five minutes because `{}` is truthy. These fields arrive only on a full register, so the
// same shape would wipe them on every lightweight refresh in between. COALESCE is what makes
// "no news" mean "unchanged" instead of "gone" — which is the property these tests pin down.

const { test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('path');
const fs = require('fs');
const os = require('os');

const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'st-hw-identity-'));
process.env.DATA_DIR = tmp;

const { db } = require('../db/database');
const { __applyHardwareIdentity: applyHardwareIdentity } = require('../ws/deviceSocket');

let n = 0;
function mkDevice() {
  const id = 'dev-hw-' + (++n);
  db.prepare("INSERT INTO devices (id,name,status,created_at) VALUES (?,?,'offline',strftime('%s','now'))")
    .run(id, 'HW ' + n);
  return id;
}
const row = (id) => db.prepare('SELECT * FROM devices WHERE id = ?').get(id);

test('the schema carries hardware identity and a temperature column', () => {
  const dev = db.prepare('PRAGMA table_info(devices)').all().map((c) => c.name);
  for (const c of ['hardware_model', 'hardware_serial', 'hardware_os_version', 'output_index']) {
    assert.ok(dev.includes(c), `devices.${c} missing`);
  }
  const tel = db.prepare('PRAGMA table_info(device_telemetry)').all().map((c) => c.name);
  assert.ok(tel.includes('temperature_c'), 'device_telemetry.temperature_c missing');
});

test('top-level bs_* fields are persisted — that is how the player reports them today', () => {
  const id = mkDevice();
  applyHardwareIdentity(id, {
    bs_model: 'XT245', bs_serial: 'URD3C6000823', bs_os_version: '9.0.189', bs_screen: 1,
  });
  const r = row(id);
  assert.equal(r.hardware_model, 'XT245');
  assert.equal(r.hardware_serial, 'URD3C6000823');
  assert.equal(r.hardware_os_version, '9.0.189');
});

test('device_info is read too, so the client can move without a flag day', () => {
  const id = mkDevice();
  applyHardwareIdentity(id, {
    device_info: { hardware_model: 'XC4055', hardware_serial: 'SN-XC', hardware_os_version: '9.1.5', output_index: 3 },
  });
  const r = row(id);
  assert.equal(r.hardware_model, 'XC4055');
  assert.equal(r.output_index, 3);
});

test('THE WIPE THIS GUARDS: a later report without the fields leaves them intact', () => {
  const id = mkDevice();
  applyHardwareIdentity(id, { bs_model: 'XT245', bs_serial: 'SN-KEEP', bs_os_version: '9.0.189' });
  // A subsequent register from a client that says nothing about hardware — the shape that nulled
  // seventeen columns when it went through the blind-overwrite path.
  applyHardwareIdentity(id, { device_info: {} });
  applyHardwareIdentity(id, {});
  const r = row(id);
  assert.equal(r.hardware_model, 'XT245', 'silence must not read as "the model is gone"');
  assert.equal(r.hardware_serial, 'SN-KEEP');
  assert.equal(r.hardware_os_version, '9.0.189');
});

test('a genuine change still overwrites — COALESCE must not freeze the value', () => {
  const id = mkDevice();
  applyHardwareIdentity(id, { bs_model: 'XT245' });
  applyHardwareIdentity(id, { bs_model: 'XC2055' });
  assert.equal(row(id).hardware_model, 'XC2055');
});

test('a player that reports none of it is not written at all', () => {
  const id = mkDevice();
  const before = row(id).updated_at;
  applyHardwareIdentity(id, { device_info: { app_version: '1.1.0-web' } });
  const r = row(id);
  assert.equal(r.hardware_model, null);
  assert.equal(r.hardware_serial, null);
  assert.equal(r.updated_at, before, 'an Android/browser register should not touch the row here');
});

test('output_index only accepts a positive integer', () => {
  const id = mkDevice();
  // screen() returns 1 for a single-output player; 0 / negative / "2" are not outputs.
  for (const bad of [0, -1, '2', 1.5, null, undefined]) {
    applyHardwareIdentity(id, { bs_model: 'X', bs_screen: bad });
    assert.equal(row(id).output_index, null, `output_index accepted ${JSON.stringify(bad)}`);
  }
  applyHardwareIdentity(id, { bs_screen: 2 });
  assert.equal(row(id).output_index, 2);
});

test('device-supplied strings are trimmed and capped', () => {
  const id = mkDevice();
  applyHardwareIdentity(id, { bs_model: '  XT245  ', bs_serial: 'S'.repeat(200) });
  const r = row(id);
  assert.equal(r.hardware_model, 'XT245');
  assert.equal(r.hardware_serial.length, 64, 'these render in the dashboard — cap them');
});

test('a whitespace-only value is not an answer', () => {
  const id = mkDevice();
  applyHardwareIdentity(id, { bs_model: 'XT245' });
  applyHardwareIdentity(id, { bs_model: '   ' });
  assert.equal(row(id).hardware_model, 'XT245');
});
