// conflict.spec.js — optimistic-concurrency save (If-Match → 412) + the
// shared conflict-resolution dialog in the browse tool.
//
// These drive the client modules directly against a stubbed fetch rather
// than a real master: the test zddc-server embeds the COMMITTED
// internal/apps/embedded/browse.html, not browse/dist/browse.html, so a
// server-mode E2E would run stale code. Loading the fresh dist build over
// file:// and stubbing fetch exercises exactly the code under test. Full
// server-mode behavior (the master's checkIfMatch → 412) is covered
// manually / on the bitnest dev server.
import { test, expect } from '@playwright/test';
import * as path from 'path';

const HTML_PATH = path.resolve('browse/dist/browse.html');

test.describe('Conflict / optimistic concurrency', () => {
    test.beforeEach(async ({ page }) => {
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'load' });
        // init.js + util.js + conflict.js run synchronously on load.
        await page.waitForFunction(
            () => window.app && window.app.modules
                && window.app.modules.util && window.app.modules.conflict);
    });

    test('saveFile sends If-Match and throws ConflictError on 412', async ({ page }) => {
        const result = await page.evaluate(async () => {
            const calls = [];
            window.fetch = async (url, opts) => {
                calls.push({ url, opts });
                return { status: 412, ok: false, headers: { get: () => null } };
            };
            window.app.state.source = 'server';
            const node = { url: '/doc.md', name: 'doc.md' };
            let status = null, name = null;
            try {
                await window.app.modules.util.saveFile(
                    node, 'hi', 'text/markdown; charset=utf-8', { etag: '"v1"' });
            } catch (e) {
                status = e.status;
                name = e.name;
            }
            return {
                sentIfMatch: calls[0] && calls[0].opts.headers['If-Match'],
                method: calls[0] && calls[0].opts.method,
                status, name
            };
        });
        expect(result.method).toBe('PUT');
        expect(result.sentIfMatch).toBe('"v1"');
        expect(result.status).toBe(412);
        expect(result.name).toBe('ConflictError');
    });

    test('saveFile returns the new ETag on success (re-edit loop)', async ({ page }) => {
        const result = await page.evaluate(async () => {
            window.fetch = async () => ({
                status: 200, ok: true,
                headers: { get: (h) => (h === 'ETag' ? '"v2"' : null) }
            });
            window.app.state.source = 'server';
            const node = { url: '/doc.md', name: 'doc.md' };
            const res = await window.app.modules.util.saveFile(
                node, 'hi', 'text/markdown; charset=utf-8', { etag: '"v1"' });
            return res;
        });
        expect(result.etag).toBe('"v2"');
    });

    test('saveFile omits the precondition when force is set', async ({ page }) => {
        const sent = await page.evaluate(async () => {
            let headers = null;
            window.fetch = async (url, opts) => {
                headers = opts.headers;
                return { status: 200, ok: true, headers: { get: () => null } };
            };
            window.app.state.source = 'server';
            await window.app.modules.util.saveFile(
                { url: '/doc.md', name: 'doc.md' }, 'hi', 'text/markdown',
                { etag: '"v1"', force: true });
            return { hasIfMatch: 'If-Match' in headers };
        });
        expect(sent.hasIfMatch).toBe(false);
    });

    test('conflict dialog renders a diff and Overwrite resolves via the callback', async ({ page }) => {
        // Kick off the dialog; stash the resolution + a flag the callback sets.
        await page.evaluate(() => {
            window.__conflict = { resolved: null, overwrote: false };
            window.app.modules.conflict.open({
                filename: 'doc.md',
                mineText: 'line one\nMINE EDIT\nline three\n',
                theirsText: 'line one\nTHEIR EDIT\nline three\n',
                onOverwrite: async () => { window.__conflict.overwrote = true; },
                onReload: async () => {},
                onSaveCopy: async () => {}
            }).then((r) => { window.__conflict.resolved = r; });
        });

        // Modal + diff present.
        const overlay = page.locator('.md-history-overlay');
        await expect(overlay).toBeVisible();
        await expect(overlay.locator('.md-diff-add').first()).toBeVisible();
        await expect(overlay.locator('.md-diff-del').first()).toBeVisible();

        // Click "Overwrite (keep mine)".
        await overlay.getByRole('button', { name: 'Overwrite (keep mine)' }).click();

        await page.waitForFunction(() => window.__conflict.resolved !== null);
        const outcome = await page.evaluate(() => window.__conflict);
        expect(outcome.resolved).toBe('overwrite');
        expect(outcome.overwrote).toBe(true);
        await expect(overlay).toBeHidden();
    });

    test('conflict dialog Cancel resolves "cancel" and runs no callback', async ({ page }) => {
        await page.evaluate(() => {
            window.__c2 = { resolved: null, ran: false };
            window.app.modules.conflict.open({
                filename: 'doc.md',
                mineText: 'a\n',
                theirsText: 'b\n',
                onOverwrite: async () => { window.__c2.ran = true; },
                onReload: async () => { window.__c2.ran = true; }
            }).then((r) => { window.__c2.resolved = r; });
        });
        const overlay = page.locator('.md-history-overlay');
        await expect(overlay).toBeVisible();
        await overlay.getByRole('button', { name: 'Cancel' }).click();
        await page.waitForFunction(() => window.__c2.resolved !== null);
        const outcome = await page.evaluate(() => window.__c2);
        expect(outcome.resolved).toBe('cancel');
        expect(outcome.ran).toBe(false);
    });
});
