import { describe, it, expect } from 'vitest';
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
    createEmpty,
    loadManifest,
    saveManifest,
    findBySignature,
    addEntry,
    markDeleted,
} from '../manifest';
import type { ManifestEntry } from '../types';

const sampleEntry = (sig: string, file = 'resources/js/A.vue'): ManifestEntry => ({
    file,
    line: 1,
    tag: 'v-btn',
    parentChain: ['A'],
    signature: sig,
    text: 'hello',
    key: null,
    ref: null,
    createdAt: '2026-05-12T00:00:00.000Z',
});

describe('manifest', () => {
    it('createEmpty returns a fresh manifest with lastId=0', () => {
        const m = createEmpty();
        expect(m.version).toBe(1);
        expect(m.lastId).toBe(0);
        expect(m.entries).toEqual({});
        expect(m.deleted).toEqual({});
    });

    it('addEntry assigns next id and increments lastId', () => {
        const m = createEmpty();
        const id1 = addEntry(m, sampleEntry('sig-1'));
        const id2 = addEntry(m, sampleEntry('sig-2'));
        expect(id1).toBe(1);
        expect(id2).toBe(2);
        expect(m.lastId).toBe(2);
        expect(m.entries['1'].signature).toBe('sig-1');
    });

    it('findBySignature returns matching id', () => {
        const m = createEmpty();
        addEntry(m, sampleEntry('sig-a'));
        addEntry(m, sampleEntry('sig-b'));
        expect(findBySignature(m, 'sig-b')).toBe(2);
        expect(findBySignature(m, 'sig-x')).toBeNull();
    });

    it('markDeleted moves entry to deleted section, preserves lastId', () => {
        const m = createEmpty();
        addEntry(m, sampleEntry('sig-x'));
        markDeleted(m, 1);
        expect(m.entries['1']).toBeUndefined();
        expect(m.deleted['1'].lastSignature).toBe('sig-x');
        expect(m.lastId).toBe(1); // monotonic, not decremented
    });

    it('deleted ids are not reused by addEntry', () => {
        const m = createEmpty();
        addEntry(m, sampleEntry('sig-x'));
        markDeleted(m, 1);
        const id = addEntry(m, sampleEntry('sig-y'));
        expect(id).toBe(2);
    });

    it('saveManifest then loadManifest roundtrips data', () => {
        const dir = mkdtempSync(join(tmpdir(), 'dx-'));
        const path = join(dir, 'm.json');
        const m = createEmpty();
        addEntry(m, sampleEntry('sig-1'));
        saveManifest(path, m);
        const loaded = loadManifest(path);
        expect(loaded.lastId).toBe(1);
        expect(loaded.entries['1'].signature).toBe('sig-1');
        rmSync(dir, { recursive: true });
    });

    it('loadManifest returns createEmpty() if file missing', () => {
        const m = loadManifest('/nonexistent/path-' + Math.random() + '.json');
        expect(m.lastId).toBe(0);
    });

    it('saveManifest writes atomically (no partial file on crash)', () => {
        const dir = mkdtempSync(join(tmpdir(), 'dx-'));
        const path = join(dir, 'm.json');
        const m = createEmpty();
        addEntry(m, sampleEntry('sig-x'));
        saveManifest(path, m);
        // Atomic write should leave no .tmp residue
        expect(() => readFileSync(path + '.tmp')).toThrow();
        rmSync(dir, { recursive: true });
    });

    it('loadManifest throws on corrupt JSON with actionable message', () => {
        const dir = mkdtempSync(join(tmpdir(), 'dx-'));
        const path = join(dir, 'm.json');
        writeFileSync(path, '{not valid json', 'utf8');
        expect(() => loadManifest(path)).toThrow(/corrupt JSON/);
        rmSync(dir, { recursive: true });
    });

    it('loadManifest throws on unsupported version', () => {
        const dir = mkdtempSync(join(tmpdir(), 'dx-'));
        const path = join(dir, 'm.json');
        writeFileSync(path, JSON.stringify({ version: 2, lastId: 0, entries: {}, deleted: {} }), 'utf8');
        expect(() => loadManifest(path)).toThrow(/version 2 unsupported/);
        rmSync(dir, { recursive: true });
    });

    it('markDeleted is a no-op for unknown ids (does not throw)', () => {
        const m = createEmpty();
        expect(() => markDeleted(m, 9999)).not.toThrow();
        expect(m.deleted['9999']).toBeUndefined();
        expect(m.lastId).toBe(0);
    });
});
