'use strict';
const test = require('node:test');
const assert = require('node:assert');
const { resolveTransitionConfig, normalizeTransitions } = require('../lib/transition-config');

const T = (cfg) => ({ widget_id: 'w', widget_type: 'transition', widget_config: JSON.stringify(cfg) });
const IMG = (id) => ({ content_id: id, mime_type: 'image/png' });

test('resolveTransitionConfig: valid shader resolves + clamps params/duration', () => {
  const r = resolveTransitionConfig({ shader: 'CRTCollapse', params: { lineHold: 99, flashGain: -5 }, durationMs: 800, scope: 'next' });
  assert.equal(r.effects.length, 1);
  assert.equal(r.effects[0].shader, 'CRTCollapse');
  assert.equal(r.durationMs, 800);
  assert.equal(r.scope, 'next');
  assert.equal(r.effects[0].params.lineHold, 0.45, 'over-range param clamped to shader max');
  assert.equal(r.effects[0].params.flashGain, 0, 'under-range param clamped to shader min');
});

test('resolveTransitionConfig: multiple shaders -> one effect each, per-shader params via map', () => {
  const r = resolveTransitionConfig({ shaders: ['CRTCollapse', 'Etch'], params: { CRTCollapse: { lineHold: 0.2 } }, durationMs: 500 });
  assert.deepEqual(r.effects.map((e) => e.shader), ['CRTCollapse', 'Etch']);
  assert.equal(r.effects[0].params.lineHold, 0.2, 'per-shader params applied from the map');
});

test('resolveTransitionConfig: unknown ids dropped from the set, dups collapsed, all-unknown -> null', () => {
  const r = resolveTransitionConfig({ shaders: ['CRTCollapse', 'Ghost', 'CRTCollapse', 'Etch'] });
  assert.deepEqual(r.effects.map((e) => e.shader), ['CRTCollapse', 'Etch']);
  assert.equal(resolveTransitionConfig({ shaders: [] }), null);
  assert.equal(resolveTransitionConfig({ shaders: ['Ghost'] }), null);
});

test('resolveTransitionConfig: unknown shader -> null (hard cut, never black)', () => {
  assert.equal(resolveTransitionConfig({ shader: 'NopeShader' }), null);
  assert.equal(resolveTransitionConfig({ shader: '' }), null);
  assert.equal(resolveTransitionConfig('not json'), null);
});

test('resolveTransitionConfig: duration bounded, scope defaults to all (one widget covers the playlist)', () => {
  assert.equal(resolveTransitionConfig({ shader: 'Etch', durationMs: 999999 }).durationMs, 3000);
  assert.equal(resolveTransitionConfig({ shader: 'Etch', durationMs: 1 }).durationMs, 150);
  assert.equal(resolveTransitionConfig({ shader: 'Etch' }).durationMs, 800, 'missing duration -> default');
  assert.equal(resolveTransitionConfig({ shader: 'Etch' }).scope, 'all', 'one widget covers the whole playlist by default');
  assert.equal(resolveTransitionConfig({ shader: 'Etch', scope: 'all' }).scope, 'all');
  assert.equal(resolveTransitionConfig({ shader: 'Etch', scope: 'next' }).scope, 'next', 'explicit next still honored');
});

test('normalizeTransitions: scope:next attaches to the FOLLOWING item, widget dropped', () => {
  const out = normalizeTransitions([IMG('a'), T({ shader: 'CRTCollapse', scope: 'next' }), IMG('b'), IMG('c')]);
  assert.equal(out.length, 3, 'transition widget removed from visible list');
  assert.deepEqual(out.map((i) => i.content_id), ['a', 'b', 'c']);
  assert.equal(out[0].transition, undefined);
  assert.equal(out[1].transition.effects[0].shader, 'CRTCollapse', 'plays INTO b');
  assert.equal(out[2].transition, undefined);
});

test('normalizeTransitions: scope:all is a playlist default, scope:next overrides it', () => {
  const out = normalizeTransitions([
    T({ shader: 'Etch', scope: 'all' }),
    IMG('a'), IMG('b'),
    T({ shader: 'CRTCollapse', scope: 'next' }), IMG('c'),
  ]);
  assert.equal(out.length, 3);
  assert.equal(out[0].transition.effects[0].shader, 'Etch', 'default applies to a');
  assert.equal(out[1].transition.effects[0].shader, 'Etch', 'default applies to b');
  assert.equal(out[2].transition.effects[0].shader, 'CRTCollapse', 'override wins for c');
});

test('normalizeTransitions: a trailing scope:next wraps onto the first item (loop)', () => {
  const out = normalizeTransitions([IMG('a'), IMG('b'), T({ shader: 'ReelChange', scope: 'next' })]);
  assert.equal(out.length, 2);
  assert.equal(out[0].transition.effects[0].shader, 'ReelChange', 'last->first advance');
  assert.equal(out[1].transition, undefined);
});

test('normalizeTransitions: unknown-shader transition widget is dropped, no transition attached', () => {
  const out = normalizeTransitions([IMG('a'), T({ shader: 'Ghost' }), IMG('b')]);
  assert.equal(out.length, 2);
  assert.equal(out[1].transition, undefined, 'invalid config -> hard cut, not a black frame');
});

test('normalizeTransitions: non-transition widgets pass through untouched', () => {
  const clock = { widget_id: 'c1', widget_type: 'clock' };
  const out = normalizeTransitions([IMG('a'), clock, T({ shader: 'Etch', scope: 'next' }), IMG('b')]);
  assert.equal(out.length, 3);
  assert.equal(out[1].widget_type, 'clock', 'clock widget stays visible');
  assert.equal(out[1].transition, undefined);
  assert.equal(out[2].transition.effects[0].shader, 'Etch');
});
