// Contract tests for progression-core's _diff(): the quest-progressed /
// path-progressed "advance" events that feed the achievement toasts, plus the
// guards that keep a completion / level-up from also firing a progress event.

const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const { ROOT } = require('./capabilities_test_harness');

const SRC = fs.readFileSync(path.join(ROOT, 'static', 'v3', 'progression-core.js'), 'utf8');

// Load progression-core.js in a sandbox whose fetch returns `states` in order.
// Boot consumes states[0] (prev=null → no diff); each later refresh() diffs
// against the previous state.
function load(states) {
    const events = [];
    let i = 0;
    const sandbox = {
        console,
        setTimeout, clearTimeout,
        fetch: async () => ({ ok: true, json: async () => states[Math.min(i++, states.length - 1)] }),
    };
    sandbox.window = sandbox;
    sandbox.window.feedBack = { emit: (name, detail) => events.push({ name, detail }) };
    sandbox.document = { readyState: 'complete', addEventListener: () => {} };
    vm.createContext(sandbox);
    vm.runInContext(SRC, sandbox);
    return { sandbox, events };
}

const stateA = {
    mastery_rank: 2,
    wallet: { balance: 100 },
    quests: {
        daily: { quests: [
            { id: 'q1', title: 'Play 3 songs', count: 1, target: 3, completed: false, reward_db: 50 },
            { id: 'q2', title: 'Finish one', count: 0, target: 1, completed: false, reward_db: 20 },
        ] },
        weekly: { quests: [
            { id: 'w1', title: 'Weekly grind', count: 2, target: 10, completed: false, reward_db: 200 },
        ] },
    },
    paths: [
        { id: 'guitar', name: 'Guitar', level: 1, max_level: 10, next: { level: 2, required: 3, completed: 1 } },
        { id: 'bass', name: 'Bass', level: 0, max_level: 10, next: { level: 1, required: 2, completed: 0 } },
    ],
};

const stateB = {
    mastery_rank: 3,                          // rank up
    wallet: { balance: 170 },                 // dB changed
    quests: {
        daily: { quests: [
            { id: 'q1', title: 'Play 3 songs', count: 2, target: 3, completed: false, reward_db: 50 },  // advanced
            { id: 'q2', title: 'Finish one', count: 1, target: 1, completed: true, reward_db: 20 },       // COMPLETED
        ] },
        weekly: { quests: [
            { id: 'w1', title: 'Weekly grind', count: 3, target: 10, completed: false, reward_db: 200 }, // advanced
        ] },
    },
    paths: [
        { id: 'guitar', name: 'Guitar', level: 1, max_level: 10, next: { level: 2, required: 3, completed: 2 } }, // progressed
        { id: 'bass', name: 'Bass', level: 1, max_level: 10, next: { level: 2, required: 3, completed: 0 } },     // LEVELED UP
    ],
};

async function diffEvents() {
    const { sandbox, events } = load([stateA, stateB]);
    await sandbox.window.v3Progression.refresh();   // coalesces with boot → state = A
    events.length = 0;                               // drop boot's progression:updated
    await sandbox.window.v3Progression.refresh();    // state = B → _diff(A, B)
    return events.filter((e) => e.name !== 'progression:updated');
}

test('quest advance emits quest-progressed with period_type, completion does not', async () => {
    const ev = await diffEvents();
    const progressed = ev.filter((e) => e.name === 'progression:quest-progressed');
    const ids = progressed.map((e) => e.detail.id).sort();
    assert.deepEqual(ids, ['q1', 'w1']);   // q2 completed → not a progress event
    const q1 = progressed.find((e) => e.detail.id === 'q1').detail;
    assert.equal(q1.period_type, 'daily');
    assert.equal(q1.count, 2);
    assert.equal(q1.target, 3);
    const w1 = progressed.find((e) => e.detail.id === 'w1').detail;
    assert.equal(w1.period_type, 'weekly');
});

test('path challenge progress emits path-progressed; a level-up does not', async () => {
    const ev = await diffEvents();
    const progressed = ev.filter((e) => e.name === 'progression:path-progressed');
    assert.equal(progressed.length, 1);
    const g = progressed[0].detail;
    assert.equal(g.id, 'guitar');
    assert.equal(g.name, 'Guitar');
    assert.equal(g.completed, 2);
    assert.equal(g.required, 3);
    assert.equal(g.next_level, 2);
    // bass leveled up (level 0 → 1) → handled by path-level-up, not path-progressed.
    assert.ok(!progressed.some((e) => e.detail.id === 'bass'));
});

test('rank-up and dB change still emit their events', async () => {
    const ev = await diffEvents();
    const rank = ev.find((e) => e.name === 'progression:rank-changed');
    assert.ok(rank && rank.detail.from === 2 && rank.detail.to === 3);
    assert.ok(ev.some((e) => e.name === 'progression:db-changed'));
});

test('no progress events fire on the very first state (prev=null)', async () => {
    const { sandbox, events } = load([stateA]);
    await sandbox.window.v3Progression.refresh();
    assert.ok(!events.some((e) => /progressed|changed/.test(e.name)));
});
