// Tests for shared/zddc-source.js — the FS-Access-API polyfill that
// lets every tool work transparently against zddc-server. The polyfill
// is the seam between every tool and the server's HTTP API; if it
// drifts, every tool's HTTP-mode behavior breaks together.
//
// Strategy: load classifier's compiled HTML over file:// (it bundles
// shared/zddc-source.js — browse, archive, landing, form do not), then
// drive the polyfill API directly. page.route() intercepts fetch() so
// we can assert request shape and stub responses without spinning up a
// real server. Cross-origin (file:// → http://) calls require CORS
// headers + OPTIONS preflight handling for non-simple requests.

import { test, expect } from '@playwright/test';
import * as path from 'path';

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

// The polyfill never inspects origin — any URL reachable by fetch()
// works. We pick one that's clearly synthetic so failures don't read
// as real network problems.
const FAKE = 'http://example.test';

// CORS headers needed because the page is on file:// and the test
// fetches a different origin. Expose every response header the
// polyfill reads (ETag, Last-Modified) so JS can see them.
const CORS = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, HEAD, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, If-Match, X-ZDDC-Op, X-ZDDC-Destination',
    'Access-Control-Expose-Headers': 'ETag, Last-Modified',
};

test.describe('shared/zddc-source.js polyfill', () => {
    test.beforeEach(async ({ page }) => {
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'load' });
    });

    test('exposes the documented surface on window.zddc.source', async ({ page }) => {
        const surface = await page.evaluate(() => {
            const s = window.zddc && window.zddc.source;
            if (!s) return null;
            return {
                hasHandleCtor: typeof s.HttpDirectoryHandle === 'function',
                hasFileCtor:   typeof s.HttpFileHandle === 'function',
                hasDetect:     typeof s.detectServerRoot === 'function',
                hasMove:       typeof s.moveFile === 'function',
                hasIsHttp:     typeof s.isHttpHandle === 'function',
                hasListing:    typeof s.httpListing === 'function',
            };
        });
        expect(surface).toEqual({
            hasHandleCtor: true,
            hasFileCtor:   true,
            hasDetect:     true,
            hasMove:       true,
            hasIsHttp:     true,
            hasListing:    true,
        });
    });

    test('detectServerRoot() returns {handle:null,status:0} on file://', async ({ page }) => {
        // The page is loaded from file:// so detect should bail out
        // immediately without hitting the network.
        const r = await page.evaluate(() => window.zddc.source.detectServerRoot());
        expect(r).toEqual({ handle: null, status: 0 });
    });

    test('httpListing() parses an array body and issues the right request', async ({ page }) => {
        const seen = [];
        await page.route(`${FAKE}/dir/`, route => {
            if (route.request().method() === 'OPTIONS') {
                route.fulfill({ status: 204, headers: CORS });
                return;
            }
            seen.push({
                method: route.request().method(),
                accept: route.request().headers()['accept'],
            });
            route.fulfill({
                status: 200,
                contentType: 'application/json',
                headers: CORS,
                body: JSON.stringify([
                    { name: 'a.txt', is_dir: false, size: 10 },
                    { name: 'sub/',  is_dir: true,  size: 0 },
                ]),
            });
        });

        const result = await page.evaluate(async (url) => {
            return await window.zddc.source.httpListing(url);
        }, `${FAKE}/dir/`);

        expect(seen.length).toBe(1);
        expect(seen[0].method).toBe('GET');
        expect(seen[0].accept).toBe('application/json');
        expect(result).toHaveLength(2);
        expect(result[0].name).toBe('a.txt');
    });

    test('httpListing() throws with .status on 403', async ({ page }) => {
        await page.route(`${FAKE}/forbidden/`, route => {
            route.fulfill({ status: 403, headers: CORS, body: 'forbidden' });
        });

        const err = await page.evaluate(async (url) => {
            try {
                await window.zddc.source.httpListing(url);
                return null;
            } catch (e) {
                return { message: e.message, status: e.status };
            }
        }, `${FAKE}/forbidden/`);

        expect(err).not.toBeNull();
        expect(err.status).toBe(403);
        expect(err.message).toMatch(/HTTP 403/);
    });

    test('HttpDirectoryHandle.values() yields polyfill handles for each entry', async ({ page }) => {
        await page.route(`${FAKE}/d/`, route => route.fulfill({
            status: 200,
            contentType: 'application/json',
            headers: CORS,
            body: JSON.stringify([
                { name: 'spec.pdf', is_dir: false, size: 1024 },
                { name: 'sub/',     is_dir: true,  size: 0 },
            ]),
        }));

        const entries = await page.evaluate(async (url) => {
            const root = new window.zddc.source.HttpDirectoryHandle(url, 'd');
            const out = [];
            for await (const entry of root.values()) {
                out.push({ name: entry.name, kind: entry.kind });
            }
            return out;
        }, `${FAKE}/d/`);

        expect(entries).toEqual([
            { name: 'spec.pdf', kind: 'file' },
            { name: 'sub',      kind: 'directory' },
        ]);
    });

    test('HttpFileHandle.getFile() fetches and surfaces ETag', async ({ page }) => {
        await page.route(`${FAKE}/d/spec.pdf`, route => route.fulfill({
            status: 200,
            contentType: 'application/pdf',
            headers: {
                ...CORS,
                'ETag': '"abc123"',
                'Last-Modified': 'Mon, 01 Jan 2024 00:00:00 GMT',
            },
            body: '%PDF-fixture',
        }));

        const out = await page.evaluate(async (url) => {
            const fh = new window.zddc.source.HttpFileHandle(url, 'spec.pdf', 12);
            const file = await fh.getFile();
            return {
                fileName: file.name,
                fileSize: file.size,
                etag: fh._etag,
            };
        }, `${FAKE}/d/spec.pdf`);

        expect(out.fileName).toBe('spec.pdf');
        expect(out.fileSize).toBeGreaterThan(0);
        expect(out.etag).toBe('abc123');
    });

    test('moveFile() POSTs with X-ZDDC-Op and X-ZDDC-Destination', async ({ page }) => {
        let captured = null;
        await page.route(`${FAKE}/old/path.txt`, route => {
            if (route.request().method() === 'OPTIONS') {
                route.fulfill({ status: 204, headers: CORS });
                return;
            }
            captured = {
                method: route.request().method(),
                op: route.request().headers()['x-zddc-op'],
                dst: route.request().headers()['x-zddc-destination'],
                ifMatch: route.request().headers()['if-match'] || null,
            };
            route.fulfill({
                status: 200,
                headers: { ...CORS, 'ETag': '"new-etag"' },
                body: '',
            });
        });

        const newEtag = await page.evaluate(async (src) => {
            return await window.zddc.source.moveFile(src, '/new/path.txt', { ifMatch: '"old"' });
        }, `${FAKE}/old/path.txt`);

        expect(captured).not.toBeNull();
        expect(captured.method).toBe('POST');
        expect(captured.op).toBe('move');
        expect(captured.dst).toBe('/new/path.txt');
        expect(captured.ifMatch).toBe('"old"');
        expect(newEtag).toBe('new-etag');
    });
});
