/**
 * Tests for the per-message Copy action in chat.js (#4659 / PR #4689).
 *
 * The Copy/Retry/Delete affordances are wired through a single delegated
 * click listener on #chat-messages (handleMessageActionClick), so we drive
 * them by building the real action-button DOM and dispatching clicks — the
 * handlers themselves are private to the chat IIFE.
 *
 * Focus: handleCopyMessage clipboard write + the transient icon swap, and a
 * regression guard for the rapid-double-click timer race (clicking copy
 * twice within 1.2s must NOT leave the button stuck on the checkmark).
 */
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';

function buildChatDom() {
    document.body.innerHTML = `
        <div id="chat-welcome"></div>
        <textarea id="chat-input"></textarea>
        <button id="send-btn"></button>
        <div id="chat-title"></div>
        <div id="chat-messages" role="log"></div>
    `;
}

/** Build an assistant message + action row matching _appendMessageActions. */
function addCopyableMessage(copyContent = 'hello world') {
    const messages = document.getElementById('chat-messages');
    const msg = document.createElement('div');
    msg.className = 'ldr-chat-message ldr-chat-message-assistant';
    const content = document.createElement('div');
    content.className = 'ldr-chat-message-content';
    const actions = document.createElement('div');
    actions.className = 'ldr-chat-message-actions';
    actions.dataset.copyContent = copyContent;
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'ldr-chat-msg-action ldr-chat-msg-action-copy';
    btn.dataset.action = 'copy';
    btn.setAttribute('aria-label', 'Copy response');
    const icon = document.createElement('i');
    icon.className = 'fas fa-copy';
    btn.appendChild(icon);
    actions.appendChild(btn);
    content.appendChild(actions);
    msg.appendChild(content);
    messages.appendChild(msg);
    return { btn, icon };
}

const flush = async (n = 6) => {
    for (let i = 0; i < n; i++) await Promise.resolve();
};

const click = (el) =>
    el.dispatchEvent(new MouseEvent('click', { bubbles: true }));

describe('chat.js — per-message Copy action', () => {
    let writeText;

    beforeEach(async () => {
        vi.resetModules();
        buildChatDom();
        writeText = vi.fn().mockResolvedValue(undefined);
        Object.defineProperty(navigator, 'clipboard', {
            value: { writeText },
            configurable: true,
        });
        Object.defineProperty(window, 'isSecureContext', {
            value: true,
            configurable: true,
        });
        // Keep init()'s awaited session restore from throwing.
        globalThis.fetch = vi.fn().mockResolvedValue({
            ok: true,
            status: 200,
            json: async () => ({ sessions: [] }),
        });
        window.ui = { renderMarkdown: (t) => t };
        window.api = { getCsrfToken: () => '' };
        window.socket = {
            on: vi.fn(),
            subscribeToResearch: vi.fn(),
            unsubscribeFromResearch: vi.fn(),
        };
        // Import AFTER the DOM + globals exist so the IIFE's init() finds
        // #chat-messages and attaches the delegated click listener.
        await import('@js/components/chat.js');
        await flush();
    });

    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
        document.body.innerHTML = '';
    });

    it('writes the message content to the clipboard and shows a checkmark', async () => {
        const { btn, icon } = addCopyableMessage('copy me please');
        click(icon);
        await flush();

        expect(writeText).toHaveBeenCalledWith('copy me please');
        expect(icon.className).toBe('fas fa-check');
        expect(btn.getAttribute('aria-label')).toBe('Copied');
    });

    it('restores the original icon and label after the 1.2s window', async () => {
        vi.useFakeTimers();
        const { btn, icon } = addCopyableMessage();
        click(icon);
        await flush();
        expect(icon.className).toBe('fas fa-check');

        vi.advanceTimersByTime(1200);
        expect(icon.className).toBe('fas fa-copy');
        expect(btn.getAttribute('aria-label')).toBe('Copy response');
    });

    it('rapid double-click does not leave the icon stuck on the checkmark', async () => {
        // Regression guard: before the fix the second click captured the
        // checkmark as the "original", so the later restore timer reverted
        // the icon to the checkmark and it stayed stuck.
        vi.useFakeTimers();
        const { icon } = addCopyableMessage();

        click(icon);
        await flush();
        vi.advanceTimersByTime(500); // second click well within the window
        click(icon);
        await flush();
        expect(icon.className).toBe('fas fa-check');

        // Let every scheduled restore fire.
        vi.advanceTimersByTime(2000);
        expect(icon.className).toBe('fas fa-copy');
    });

    it('falls back to execCommand when the async clipboard is unavailable', async () => {
        Object.defineProperty(navigator, 'clipboard', {
            value: undefined,
            configurable: true,
        });
        const exec = vi.fn().mockReturnValue(true);
        document.execCommand = exec;
        const { icon } = addCopyableMessage('legacy path');
        click(icon);
        await flush();

        expect(exec).toHaveBeenCalledWith('copy');
        expect(icon.className).toBe('fas fa-check');
    });
});
