'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');

const { StatusUpdater } = require('../../src/nodered/statusUpdater');

function makeNode() {
    const calls = [];
    return {
        calls,
        status(badge) { calls.push(badge); },
    };
}

function makeSource(initial) {
    return {
        badge: initial,
        throwOnNext: false,
        getStatusBadge() {
            if (this.throwOnNext) {
                this.throwOnNext = false;
                throw new Error('boom');
            }
            return this.badge;
        },
    };
}

function makeLogger() {
    const errors = [];
    return {
        errors,
        error(msg) { errors.push(msg); },
    };
}

test('start() schedules a tick that applies the source badge', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
    const u = new StatusUpdater({ node, source, intervalMs: 1000 });
    u.start();
    assert.equal(node.calls.length, 0);
    t.mock.timers.tick(1000);
    assert.equal(node.calls.length, 1);
    assert.deepEqual(node.calls[0], { fill: 'green', shape: 'dot', text: 'OK' });
    u.stop();
});

test('multiple ticks reflect the latest badge from the source', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const source = makeSource({ fill: 'green', shape: 'dot', text: 'A' });
    const u = new StatusUpdater({ node, source, intervalMs: 500 });
    u.start();
    t.mock.timers.tick(500);
    source.badge = { fill: 'yellow', shape: 'dot', text: 'B' };
    t.mock.timers.tick(500);
    source.badge = { fill: 'red', shape: 'ring', text: 'C' };
    t.mock.timers.tick(500);
    assert.equal(node.calls.length, 3);
    assert.equal(node.calls[0].text, 'A');
    assert.equal(node.calls[1].text, 'B');
    assert.equal(node.calls[2].text, 'C');
    u.stop();
});

test('source returns null → node.status({}) is called', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const source = makeSource(null);
    const u = new StatusUpdater({ node, source, intervalMs: 100 });
    u.start();
    t.mock.timers.tick(100);
    assert.equal(node.calls.length, 1);
    assert.deepEqual(node.calls[0], {});
    u.stop();
});

test('source throw → error logged, error badge applied, next tick still runs', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const logger = makeLogger();
    const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
    source.throwOnNext = true;
    const u = new StatusUpdater({ node, source, intervalMs: 1000, logger });
    u.start();
    t.mock.timers.tick(1000);
    assert.equal(logger.errors.length, 1, 'error logged once');
    assert.match(logger.errors[0], /boom/);
    assert.deepEqual(node.calls[0], { fill: 'red', shape: 'ring', text: '⚠ boom' });
    // Subsequent tick: source recovers, normal badge resumes.
    t.mock.timers.tick(1000);
    assert.equal(node.calls.length, 2);
    assert.deepEqual(node.calls[1], { fill: 'green', shape: 'dot', text: 'OK' });
    u.stop();
});

test('stop() halts the interval AND clears the badge', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
    const u = new StatusUpdater({ node, source, intervalMs: 500 });
    u.start();
    t.mock.timers.tick(500);
    assert.equal(node.calls.length, 1);
    u.stop();
    assert.equal(u.isRunning, false);
    // stop() pushes a clear-badge call.
    assert.equal(node.calls.length, 2);
    assert.deepEqual(node.calls[1], {});
    // No further ticks after stop.
    t.mock.timers.tick(5000);
    assert.equal(node.calls.length, 2);
});

test('start() called twice does not schedule two intervals', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
    const u = new StatusUpdater({ node, source, intervalMs: 1000 });
    u.start();
    u.start();
    u.start();
    t.mock.timers.tick(1000);
    assert.equal(node.calls.length, 1, 'one tick per interval period');
    t.mock.timers.tick(1000);
    assert.equal(node.calls.length, 2);
    u.stop();
});

test('intervalMs: 0 makes start() a no-op', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
    const u = new StatusUpdater({ node, source, intervalMs: 0 });
    u.start();
    assert.equal(u.isRunning, false);
    t.mock.timers.tick(10000);
    assert.equal(node.calls.length, 0);
});

test('intervalMs omitted is also treated as a no-op', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
    const u = new StatusUpdater({ node, source });
    u.start();
    assert.equal(u.isRunning, false);
    t.mock.timers.tick(10000);
    assert.equal(node.calls.length, 0);
});

test('constructor throws if node.status is missing', () => {
    const source = makeSource(null);
    assert.throws(
        () => new StatusUpdater({ node: {}, source, intervalMs: 1000 }),
        /node must expose a \.status/,
    );
    assert.throws(
        () => new StatusUpdater({ node: null, source, intervalMs: 1000 }),
        /node must expose a \.status/,
    );
});

test('constructor throws if source.getStatusBadge is missing', () => {
    const node = makeNode();
    assert.throws(
        () => new StatusUpdater({ node, source: {}, intervalMs: 1000 }),
        /source must expose a \.getStatusBadge/,
    );
    assert.throws(
        () => new StatusUpdater({ node, source: null, intervalMs: 1000 }),
        /source must expose a \.getStatusBadge/,
    );
});

test('isRunning getter reflects timer lifecycle', (t) => {
    t.mock.timers.enable({ apis: ['setInterval'] });
    const node = makeNode();
    const source = makeSource(null);
    const u = new StatusUpdater({ node, source, intervalMs: 1000 });
    assert.equal(u.isRunning, false);
    u.start();
    assert.equal(u.isRunning, true);
    u.stop();
    assert.equal(u.isRunning, false);
});
