import { test } from 'node:test';
import assert from 'node:assert/strict';
import { once } from 'node:events';
import WebSocket from 'ws';
import { randomUUID } from 'node:crypto';
import { fixture, login, post, password } from './fixture.js';

async function connect(t, origin, cookie, suppliedOrigin = origin) {
    const socket = new WebSocket(`${origin.replace('http', 'ws')}/match?redwebVersion=1`, { headers: { Cookie: cookie, Origin: suppliedOrigin } });
    t.after(() => socket.terminate());
    const messages = [];
    socket.on('message', data => messages.push(JSON.parse(data.toString())));
    await once(socket, 'open');
    return { socket, messages, async send(type, payload, expected = 'state') {
        const cursor = messages.length;
        const requestId = randomUUID();
        socket.send(JSON.stringify({ v: '1', type, payload, requestId }));
        const signal = AbortSignal.timeout(5000);
        const matches = message => message.type === expected && message.requestId === requestId;
        while (!messages.slice(cursor).some(matches)) await once(socket, 'message', { signal });
        return messages.slice(cursor).find(matches);
    } };
}

test('real HTTP and WebSockets: authentication, two-player game, attacks, reconnect, logout', { timeout: 20000 }, async t => {
    const f = await fixture(); t.after(() => f.close());
    assert.equal((await fetch(f.origin, { redirect: 'manual' })).status, 401);
    assert.equal((await post(f.origin, '/login', { account: 'alice', password }, undefined, 'https://evil.example')).status, 403);
    assert.equal((await post(f.origin, '/login', { account: 'alice', password: 'wrong-password-value' })).status, 401);
    const aliceCookie = await login(f.origin, 'alice'), bobCookie = await login(f.origin, 'bob'), carolCookie = await login(f.origin, 'carol');
    const html = await (await fetch(f.origin, { headers: { Cookie: aliceCookie } })).text();
    assert.match(html, /data-cell="8"/); assert.match(html, /src="\/client.js"/);
    assert.doesNotMatch(html, /__redweb_page/);
    assert.equal((await fetch(`${f.origin}/client.js`)).status, 200);
    assert.equal((await fetch(`${f.origin}/game.css`)).status, 200);
    await assert.rejects(connect(t, f.origin, aliceCookie, 'https://evil.example'), /403/);
    await assert.rejects(connect(t, f.origin, 'redweb_dashboard=invalid'), /401|403/);
    const alice = await connect(t, f.origin, aliceCookie), bob = await connect(t, f.origin, bobCookie), carol = await connect(t, f.origin, carolCookie);
    assert.match((await alice.send('move', { cell: 0, revision: 0 }, 'notice')).payload.text, /Join/);
    assert.match((await alice.send('resume', { room: 'none' }, 'notice')).payload.text, /not found/);
    assert.equal((await alice.send('join', { room: 'first' })).payload.you, 'X');
    assert.equal((await bob.send('join', { room: 'first' })).payload.you, 'O');
    assert.match((await carol.send('resume', { room: 'first' }, 'notice')).payload.text, /Join/);
    assert.match((await carol.send('join', { room: 'first' }, 'notice')).payload.text, /full/);
    assert.match((await alice.send('join', { room: 'other' }, 'notice')).payload.text, /Reload/);
    assert.match((await bob.send('move', { cell: 0, revision: 0 }, 'notice')).payload.text, /turn/);
    await alice.send('move', { cell: 0, revision: 0 });
    assert.match((await bob.send('move', { cell: 1, revision: 0 }, 'notice')).payload.text, /changed/);
    await bob.send('move', { cell: 3, revision: 1 });
    alice.socket.close(); await once(alice.socket, 'close');
    const returned = await connect(t, f.origin, aliceCookie);
    const restored = await returned.send('resume', { room: 'first' });
    assert.equal(restored.payload.revision, 2);
    await returned.send('move', { cell: 1, revision: 2 });
    await bob.send('move', { cell: 4, revision: 3 });
    assert.equal((await returned.send('move', { cell: 2, revision: 4 })).payload.result, 'X');
    const closed = once(returned.socket, 'close');
    assert.equal((await post(f.origin, '/logout', {}, aliceCookie)).status, 303);
    assert.equal((await closed)[0], 1008);
    await assert.rejects(connect(t, f.origin, aliceCookie), /401|403/);
    const invalid = await carol.send('move', { cell: 9, revision: 0 }, 'error');
    assert.equal(invalid.error.code, 'INVALID_PAYLOAD');
});
