/**
 * Tests for deletion modules — confirmation_modal.js and delete_manager.js
 *
 * Focuses on actual workflow behavior: API calls, error handling,
 * success/failure callbacks, preview-driven confirmation messages,
 * and the formatBytes utility. Skips trivial "API shape" assertions.
 */

import '@js/deletion/confirmation_modal.js';

describe('DeleteConfirmation tooltip lookup', () => {
    it('returns the configured tooltip for a known action', () => {
        // We test that lookup works without asserting exact tooltip text
        const tooltip = window.DeleteConfirmation.getTooltip('deleteDocument');
        expect(tooltip).toBeTypeOf('string');
        expect(tooltip.length).toBeGreaterThan(0);
    });

    it('returns a generic fallback for unknown actions', () => {
        expect(window.DeleteConfirmation.getTooltip('unknownAction')).toBe('Delete this item');
    });

    it('marks all destructive actions as dangerous', () => {
        const actions = window.DeleteConfirmation.ACTIONS;
        // Behavioral invariant: deletion actions whose name starts with "delete"
        // and which permanently destroy data must be flagged dangerous.
        expect(actions.deleteDocument.dangerous).toBe(true);
        expect(actions.deleteCollection.dangerous).toBe(true);
        expect(actions.bulkDeleteDocuments.dangerous).toBe(true);
    });

    it('marks recoverable actions as not dangerous', () => {
        const actions = window.DeleteConfirmation.ACTIONS;
        // Removing a PDF blob keeps the text content searchable — recoverable.
        expect(actions.deleteBlob.dangerous).toBe(false);
        expect(actions.removeFromCollection.dangerous).toBe(false);
    });
});

// Now load delete_manager and stub its dependencies before importing
let DM;

beforeAll(async () => {
    window.api = {
        fetchWithErrorHandling: vi.fn(),
        getCsrfToken: () => 'test-token',
    };
    window.showToast = vi.fn();

    // Auto-confirm: bypass the modal and just run the action immediately.
    // This lets us test the actual API workflow without the modal UI.
    window.DeleteConfirmation.confirmAndRun = vi.fn(async (opts, action) => {
        await action();
    });

    await import('@js/deletion/delete_manager.js');
    DM = window.DeleteManager;
});

beforeEach(() => {
    window.api.fetchWithErrorHandling.mockReset();
    window.showToast.mockReset();
    window.DeleteConfirmation.confirmAndRun.mockClear();
});

// Flush pending microtasks. delete_manager's skipConfirm path calls
// confirmOptions.onConfirm() WITHOUT awaiting it, so test callers need
// to wait for the inner async work to settle. This is a small wart in
// the source — flushPromises() is the standard workaround.
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));

describe('DeleteManager.formatBytes', () => {
    it('formats 0 bytes', () => {
        expect(DM.formatBytes(0)).toBe('0 Bytes');
    });

    it('formats bytes < 1024', () => {
        expect(DM.formatBytes(500)).toBe('500 Bytes');
    });

    it('formats kilobytes with decimal precision', () => {
        expect(DM.formatBytes(1024)).toBe('1 KB');
        expect(DM.formatBytes(1536)).toBe('1.5 KB');
    });

    it('formats megabytes', () => {
        expect(DM.formatBytes(1048576)).toBe('1 MB');
    });

    it('formats gigabytes', () => {
        expect(DM.formatBytes(1073741824)).toBe('1 GB');
    });

    it('chooses the right unit for boundary values', () => {
        expect(DM.formatBytes(1023)).toContain('Bytes');
        expect(DM.formatBytes(1024)).toContain('KB');
        expect(DM.formatBytes(1024 * 1024 - 1)).toContain('KB');
        expect(DM.formatBytes(1024 * 1024)).toContain('MB');
    });
});

describe('DeleteManager.deleteDocument', () => {
    it('calls DELETE on the document endpoint', async () => {
        // First call: preview lookup (GET). Second call: actual DELETE.
        window.api.fetchWithErrorHandling
            .mockResolvedValueOnce({ success: true, title: 'My Doc', has_blob: false, has_text: false })
            .mockResolvedValueOnce({ success: true });

        await DM.deleteDocument('doc-123');

        // Verify a DELETE was made to the right endpoint
        const deleteCall = window.api.fetchWithErrorHandling.mock.calls.find(
            ([_url, opts]) => opts && opts.method === 'DELETE'
        );
        expect(deleteCall).toBeDefined();
        expect(deleteCall[0]).toContain('doc-123');
    });

    it('skips the preview when skipConfirm is true', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({ success: true });

        await DM.deleteDocument('doc-456', { skipConfirm: true });
        await flushPromises();

        // Only one call (the DELETE), no preview GET
        expect(window.api.fetchWithErrorHandling).toHaveBeenCalledTimes(1);
        expect(window.api.fetchWithErrorHandling.mock.calls[0][1].method).toBe('DELETE');
    });

    it('calls onSuccess callback with API result on success', async () => {
        const onSuccess = vi.fn();
        const apiResult = { success: true, deleted_id: 'doc-789' };
        window.api.fetchWithErrorHandling.mockResolvedValueOnce(apiResult);

        await DM.deleteDocument('doc-789', { skipConfirm: true, onSuccess });
        await flushPromises();

        expect(onSuccess).toHaveBeenCalledWith(apiResult);
    });

    it('calls onError callback when API returns success: false', async () => {
        const onError = vi.fn();
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: false,
            error: 'Document is locked',
        });

        await DM.deleteDocument('doc-locked', { skipConfirm: true, onError });
        await flushPromises();

        expect(onError).toHaveBeenCalled();
        const err = onError.mock.calls[0][0];
        expect(err.message).toContain('Document is locked');
    });

    it('calls onError when fetch throws', async () => {
        const onError = vi.fn();
        window.api.fetchWithErrorHandling.mockRejectedValueOnce(new Error('Network down'));

        await DM.deleteDocument('doc-net', { skipConfirm: true, onError });
        await flushPromises();

        expect(onError).toHaveBeenCalled();
    });

    it('does not call onSuccess when API call fails', async () => {
        const onSuccess = vi.fn();
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({ success: false, error: 'fail' });

        await DM.deleteDocument('doc-x', { skipConfirm: true, onSuccess, onError: () => {} });

        expect(onSuccess).not.toHaveBeenCalled();
    });
});

describe('DeleteManager.deleteDocumentBlob', () => {
    it('calls DELETE on the document blob endpoint', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({ success: true, bytes_freed: 2048 });

        await DM.deleteDocumentBlob('doc-1', { skipConfirm: true });
        await flushPromises();

        const call = window.api.fetchWithErrorHandling.mock.calls[0];
        expect(call[0]).toContain('doc-1');
        expect(call[0]).toContain('blob');
        expect(call[1].method).toBe('DELETE');
    });

    it('passes bytes_freed to onSuccess', async () => {
        const onSuccess = vi.fn();
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            bytes_freed: 5000,
        });

        await DM.deleteDocumentBlob('doc-1', { skipConfirm: true, onSuccess });
        await flushPromises();

        expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({ bytes_freed: 5000 }));
    });
});

describe('DeleteManager.removeFromCollection', () => {
    it('calls DELETE on the collection-document endpoint with both IDs', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            document_deleted: false,
        });

        await DM.removeFromCollection('doc-1', 'coll-2', { skipConfirm: true });
        await flushPromises();

        const call = window.api.fetchWithErrorHandling.mock.calls[0];
        expect(call[0]).toContain('doc-1');
        expect(call[0]).toContain('coll-2');
        expect(call[1].method).toBe('DELETE');
    });

    it('handles document_deleted: true (last collection removal)', async () => {
        const onSuccess = vi.fn();
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            document_deleted: true,
        });

        await DM.removeFromCollection('doc-1', 'coll-2', { skipConfirm: true, onSuccess });
        await flushPromises();

        expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({ document_deleted: true }));
    });
});

describe('DeleteManager.deleteCollection', () => {
    it('calls DELETE on the collection endpoint', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            documents_unlinked: 5,
            chunks_deleted: 100,
        });

        await DM.deleteCollection('coll-99', { skipConfirm: true });
        await flushPromises();

        const call = window.api.fetchWithErrorHandling.mock.calls[0];
        expect(call[0]).toContain('coll-99');
        expect(call[1].method).toBe('DELETE');
    });

    it('reports failure via onError', async () => {
        const onError = vi.fn();
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: false,
            error: 'Collection in use',
        });

        await DM.deleteCollection('coll-used', { skipConfirm: true, onError });
        await flushPromises();

        expect(onError).toHaveBeenCalled();
    });
});

describe('DeleteManager.deleteCollectionIndex', () => {
    it('calls DELETE on the index endpoint', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            chunks_deleted: 42,
        });

        await DM.deleteCollectionIndex('coll-1', { skipConfirm: true });
        await flushPromises();

        const call = window.api.fetchWithErrorHandling.mock.calls[0];
        expect(call[0]).toContain('coll-1');
        expect(call[0]).toContain('index');
        expect(call[1].method).toBe('DELETE');
    });
});

describe('DeleteManager.bulkDeleteDocuments', () => {
    it('sends document IDs in the request body', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            deleted: 3,
            total: 3,
            total_bytes_freed: 1024,
        });

        await DM.bulkDeleteDocuments(['a', 'b', 'c'], { skipConfirm: true });
        await flushPromises();

        const call = window.api.fetchWithErrorHandling.mock.calls[0];
        expect(call[1].method).toBe('DELETE');
        const body = JSON.parse(call[1].body);
        expect(body.document_ids).toEqual(['a', 'b', 'c']);
    });

    it('reports partial-success counts via onSuccess', async () => {
        const onSuccess = vi.fn();
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            deleted: 2,
            total: 3,
            total_bytes_freed: 2048,
        });

        await DM.bulkDeleteDocuments(['a', 'b', 'c'], { skipConfirm: true, onSuccess });
        await flushPromises();

        expect(onSuccess).toHaveBeenCalledWith(
            expect.objectContaining({ deleted: 2, total: 3 })
        );
    });
});

describe('DeleteManager.bulkDeleteBlobs', () => {
    it('sends document IDs in the request body', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            deleted: 2,
            total_bytes_freed: 1024,
        });

        await DM.bulkDeleteBlobs(['x', 'y'], { skipConfirm: true });
        await flushPromises();

        const call = window.api.fetchWithErrorHandling.mock.calls[0];
        const body = JSON.parse(call[1].body);
        expect(body.document_ids).toEqual(['x', 'y']);
    });
});

describe('DeleteManager.bulkRemoveFromCollection', () => {
    it('sends collection ID in URL and document IDs in body', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({
            success: true,
            unlinked: 3,
            deleted: 1,
        });

        await DM.bulkRemoveFromCollection(['a', 'b', 'c'], 'coll-7', { skipConfirm: true });
        await flushPromises();

        const call = window.api.fetchWithErrorHandling.mock.calls[0];
        expect(call[0]).toContain('coll-7');
        expect(call[1].method).toBe('DELETE');
        const body = JSON.parse(call[1].body);
        expect(body.document_ids).toEqual(['a', 'b', 'c']);
    });
});

describe('DeleteManager confirmation flow', () => {
    it('skipConfirm: true bypasses DeleteConfirmation.confirmAndRun', async () => {
        window.api.fetchWithErrorHandling.mockResolvedValueOnce({ success: true });

        await DM.deleteDocument('doc-1', { skipConfirm: true });
        await flushPromises();

        expect(window.DeleteConfirmation.confirmAndRun).not.toHaveBeenCalled();
    });

    it('skipConfirm: false routes through confirmation', async () => {
        // The first call is the preview lookup
        window.api.fetchWithErrorHandling
            .mockResolvedValueOnce({ success: true, title: 'Doc' })
            .mockResolvedValueOnce({ success: true });

        await DM.deleteDocument('doc-2');

        expect(window.DeleteConfirmation.confirmAndRun).toHaveBeenCalled();
    });

    it('preview failure does not block the deletion (warning only)', async () => {
        // Preview throws, but the deletion should still proceed via confirmAndRun
        window.api.fetchWithErrorHandling
            .mockRejectedValueOnce(new Error('preview failed'))
            .mockResolvedValueOnce({ success: true });

        await DM.deleteDocument('doc-3');

        // confirmAndRun was still called → user still gets the dialog
        expect(window.DeleteConfirmation.confirmAndRun).toHaveBeenCalled();
    });
});
