#!/usr/bin/env node
/**
 * Follow-up Research UI Tests
 *
 * Tests for the follow-up research functionality from existing research results.
 *
 * Run: node test_followup_research_ci.js
 */

const { setupTest, teardownTest, TestResults, log, delay, navigateTo, withTimeout } = require('./test_lib');

/**
 * Navigate with a single retry on timeout.
 *
 * In CI the server can be slow after previous tests finished (heavy DB
 * operations, template rendering). A one-shot retry avoids a cascade of
 * "detached frame" failures that would otherwise mark every remaining
 * sub-test as broken.
 */
async function navigateToWithRetry(page, url) {
    try {
        await navigateTo(page, url);
    } catch (firstError) {
        // Retry once after a short pause
        await delay(2000);
        await navigateTo(page, url);
    }
}

/**
 * Deterministic results-page URL for follow-up modal tests.
 *
 * The /results/<id> route (research_routes.py: results_page) renders
 * pages/results.html without any DB lookup of the research id — it only
 * carries @login_required (satisfied here by setupTest({authenticate:true})),
 * so a synthetic UUID still renders the page, the
 * #ask-followup-btn, and loads followup.js.  This lets the follow-up
 * modal tests run on a no-LLM CI shard with a fresh DB (where no
 * completed research exists to link to), instead of skipping the whole
 * feature.  followup.js derives parentResearchId from this URL path.
 *
 * Uses the reserved nil UUID: it is never assigned to a real research record,
 * so it can't collide with a seeded fixture and silently exercise a different
 * code path.
 */
const SYNTHETIC_RESULTS_ID = '00000000-0000-0000-0000-000000000000';

/**
 * Open the follow-up modal on a freshly-loaded results page and wait for
 * it to be visible.  Returns true once #followUpModal is shown.
 *
 * The modal markup is fetched client-side from
 * /static/templates/followup_modal.html and injected by followup.js, then
 * shown via Bootstrap (class "show", display !== none).  We poll because
 * the fetch + insert is async.
 */
async function openFollowupModal(page, baseUrl) {
    // Force a genuinely fresh load of the results page on every call.
    // navigateTo() skips page.goto when the path is unchanged, so without
    // this reset a test that runs after another already on /results/<id>
    // would re-use the modal left open by the earlier test instead of
    // independently re-loading the page and re-opening the modal.  Resetting
    // to about:blank guarantees each test exercises the full
    // navigate -> inject -> show path from a clean page.
    await page.goto('about:blank', { timeout: 5000 });

    await navigateToWithRetry(page, `${baseUrl}/results/${SYNTHETIC_RESULTS_ID}`);

    // followup.js wires #ask-followup-btn.onclick on DOMContentLoaded.
    // Wait for the button to exist and be enabled before clicking.
    await page.waitForSelector('#ask-followup-btn:not([disabled])', { timeout: 10000 });

    await page.evaluate(() => {
        const btn = document.getElementById('ask-followup-btn');
        if (window.followUpResearch) {
            window.followUpResearch.showFollowUpModal();
        } else if (btn) {
            btn.click();
        }
    });

    // Wait for the modal to be injected and shown.
    try {
        await page.waitForFunction(() => {
            const m = document.getElementById('followUpModal');
            return !!m && getComputedStyle(m).display !== 'none';
        }, { timeout: 8000 });
        return true;
    } catch (e) {
        log(`openFollowupModal: #followUpModal did not become visible within 8s (${e.message})`);
        return false;
    }
}

// ============================================================================
// Follow-up Research Tests
// ============================================================================
const FollowupResearchTests = {
    async followupButtonOnResults(page, baseUrl) {
        // First find a completed research
        await navigateToWithRetry(page, `${baseUrl}/history`);

        const researchId = await page.evaluate(() => {
            // Look for completed research with results link
            const resultsLink = document.querySelector('a[href*="/results/"]');
            if (resultsLink) {
                const match = resultsLink.href.match(/\/results\/([a-zA-Z0-9-]+)/);
                return match ? match[1] : null;
            }

            // Try data attributes
            const item = document.querySelector('[data-research-id], [data-id]');
            return item?.dataset?.researchId || item?.dataset?.id;
        });

        if (!researchId) {
            return { passed: null, skipped: true, message: 'No completed research found to test follow-up' };
        }

        await navigateToWithRetry(page, `${baseUrl}/results/${researchId}`);

        const result = await page.evaluate(() => {
            const buttons = Array.from(document.querySelectorAll('button, a.btn, .btn'));
            const followupBtn = buttons.find(b => {
                const text = b.textContent?.toLowerCase() || '';
                return text.includes('follow') || text.includes('continue') ||
                       text.includes('deeper') || text.includes('expand');
            });

            return {
                hasFollowupButton: !!followupBtn,
                buttonText: followupBtn?.textContent?.trim()
            };
        });

        if (!result.hasFollowupButton) {
            return { passed: null, skipped: true, message: 'No follow-up button found on results page' };
        }

        return {
            passed: true,
            message: `Follow-up button found: "${result.buttonText}"`
        };
    },

    async followupModalOpens(page, baseUrl) {
        // Navigate to a results page
        await navigateToWithRetry(page, `${baseUrl}/history`);

        const researchId = await page.evaluate(() => {
            const link = document.querySelector('a[href*="/results/"]');
            const match = link?.href?.match(/\/results\/([a-zA-Z0-9-]+)/);
            return match ? match[1] : null;
        });

        if (!researchId) {
            return { passed: null, skipped: true, message: 'No completed research for follow-up modal test' };
        }

        await navigateToWithRetry(page, `${baseUrl}/results/${researchId}`);

        // Click follow-up button
        const clicked = await page.evaluate(() => {
            const buttons = Array.from(document.querySelectorAll('button, a.btn'));
            const followupBtn = buttons.find(b => {
                const text = b.textContent?.toLowerCase() || '';
                return text.includes('follow') || text.includes('continue') || text.includes('deeper');
            });

            if (followupBtn) {
                followupBtn.click();
                return true;
            }
            return false;
        });

        if (!clicked) {
            return { passed: null, skipped: true, message: 'No follow-up button to click' };
        }

        await delay(500);

        const result = await page.evaluate(() => {
            const modal = document.querySelector('.modal, .dialog, [role="dialog"], .followup-form');
            const form = document.querySelector('form.followup-form, form[action*="followup"], .followup-modal form');

            return {
                hasModal: !!modal && (modal.style.display !== 'none'),
                hasForm: !!form,
                hasQueryInput: !!document.querySelector('input[name*="query"], textarea[name*="query"], #followup-query')
            };
        });

        const passed = result.hasModal || result.hasForm || result.hasQueryInput;

        return {
            passed,
            message: passed
                ? `Follow-up modal/form opens (modal=${result.hasModal}, form=${result.hasForm})`
                : 'Follow-up modal did not open'
        };
    },

    async followupQueryPrefilled(page, baseUrl) {
        const opened = await openFollowupModal(page, baseUrl);
        if (!opened) {
            return { passed: false, message: 'Follow-up modal did not open on results page' };
        }

        // The follow-up textarea is #followUpQuestion inside #followUpModal.
        // It is NOT prefilled with a value (followup.js only ever reads
        // .value, never writes it); it carries an example-prompt
        // placeholder and is `required`.  Assert that concrete structure
        // scoped to the modal so a wrong page/modal would fail this test.
        const result = await page.evaluate(() => {
            const modal = document.getElementById('followUpModal');
            if (!modal) return { hasModal: false };

            const input = modal.querySelector('textarea#followUpQuestion');
            if (!input) return { hasModal: true, hasInput: false };

            return {
                hasModal: true,
                hasInput: true,
                placeholder: input.placeholder || '',
                placeholderLen: (input.placeholder || '').length,
                required: input.hasAttribute('required'),
                value: input.value || ''
            };
        });

        if (!result.hasModal) {
            return { passed: false, message: 'Wrong page loaded: #followUpModal missing' };
        }
        if (!result.hasInput) {
            return { passed: false, message: 'Follow-up modal opened but textarea#followUpQuestion not found' };
        }

        // Real assertion: the question field renders with a non-trivial
        // example placeholder and is required.  Empty value is expected
        // (the field is not prefilled), so we do not require a value.
        const passed = result.placeholderLen > 10 && result.required && result.value === '';

        return {
            passed,
            message: passed
                ? `Follow-up question field ready (required, placeholder ${result.placeholderLen} chars, value empty as expected)`
                : `Unexpected question field state: required=${result.required}, placeholderLen=${result.placeholderLen}, value="${result.value.slice(0, 30)}"`
        };
    },

    async followupSubmitButton(page, baseUrl) {
        const opened = await openFollowupModal(page, baseUrl);
        if (!opened) {
            return { passed: false, message: 'Follow-up modal did not open on results page' };
        }

        // Assert the submit control SCOPED to #followUpModal — no global
        // .btn-primary fallback (the results page chrome also has
        // .btn-primary buttons like #ask-followup-btn).  The modal's
        // submit button is the primary button that triggers
        // submitFollowUp() and is labelled "Start Follow-up Research".
        const result = await page.evaluate(() => {
            const modal = document.getElementById('followUpModal');
            if (!modal) return { hasModal: false };

            const footer = modal.querySelector('.modal-footer') || modal;
            const candidates = Array.from(footer.querySelectorAll('button.btn-primary'));
            const submitBtn = candidates.find(b =>
                (b.getAttribute('onclick') || '').includes('submitFollowUp') ||
                /follow-?up/i.test(b.textContent || '')
            ) || candidates[0];

            if (!submitBtn) {
                return { hasModal: true, hasSubmitBtn: false };
            }

            return {
                hasModal: true,
                hasSubmitBtn: true,
                buttonText: (submitBtn.textContent || '').trim(),
                onclick: submitBtn.getAttribute('onclick') || '',
                isDisabled: !!submitBtn.disabled
            };
        });

        if (!result.hasModal) {
            return { passed: false, message: 'Wrong page loaded: #followUpModal missing' };
        }
        if (!result.hasSubmitBtn) {
            return { passed: false, message: 'No .btn-primary submit button inside #followUpModal' };
        }

        // Real assertion: the scoped button is the follow-up submit
        // control (wired to submitFollowUp and/or labelled accordingly).
        const wired = result.onclick.includes('submitFollowUp');
        const labelled = /follow-?up/i.test(result.buttonText);
        const passed = wired || labelled;

        return {
            passed,
            message: passed
                ? `Follow-up submit button found in modal: "${result.buttonText}" (disabled: ${result.isDisabled})`
                : `Found modal .btn-primary but it is not the follow-up submit: "${result.buttonText}" onclick="${result.onclick}"`
        };
    },

    async followupModeSelection(page, baseUrl) {
        await navigateToWithRetry(page, `${baseUrl}/history`);

        const researchId = await page.evaluate(() => {
            const link = document.querySelector('a[href*="/results/"]');
            const match = link?.href?.match(/\/results\/([a-zA-Z0-9-]+)/);
            return match ? match[1] : null;
        });

        if (!researchId) {
            return { passed: null, skipped: true, message: 'No research for mode selection test' };
        }

        await navigateToWithRetry(page, `${baseUrl}/results/${researchId}`);

        // Open follow-up form
        await page.evaluate(() => {
            const buttons = Array.from(document.querySelectorAll('button, a.btn'));
            const followupBtn = buttons.find(b =>
                b.textContent?.toLowerCase().includes('follow') ||
                b.textContent?.toLowerCase().includes('deeper')
            );
            if (followupBtn) followupBtn.click();
        });

        await delay(500);

        const result = await page.evaluate(() => {
            const modeSelect = document.querySelector(
                'select[name*="mode"], ' +
                '#research-mode, ' +
                '.mode-select'
            );

            if (modeSelect) {
                const options = Array.from(modeSelect.options).map(o => o.text);
                return {
                    exists: true,
                    type: 'select',
                    options: options.slice(0, 5)
                };
            }

            // Check for radio buttons or toggle
            const modeRadios = document.querySelectorAll('input[type="radio"][name*="mode"]');
            const modeToggle = document.querySelector('.mode-toggle, [class*="mode-selector"]');

            if (modeRadios.length > 0) {
                return {
                    exists: true,
                    type: 'radio',
                    optionCount: modeRadios.length
                };
            }

            if (modeToggle) {
                return {
                    exists: true,
                    type: 'toggle'
                };
            }

            return { exists: false };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'No mode selection in follow-up form' };
        }

        return {
            passed: true,
            message: result.type === 'select'
                ? `Mode selection: ${result.options.join(', ')}`
                : `Mode selection (${result.type})`
        };
    },

    async followupLinksToOriginal(page, baseUrl) {
        const opened = await openFollowupModal(page, baseUrl);
        if (!opened) {
            return { passed: false, message: 'Follow-up modal did not open on results page' };
        }

        // The follow-up modal references the parent/original research via a
        // dedicated #parentContext block (populated by /api/followup/prepare)
        // plus visible copy stating the follow-up builds on the *previous
        // research* context.  There is no static hyperlink to the source id;
        // the parent linkage is carried in JS (parentResearchId from the URL)
        // and surfaced through #parentContext / #parentSummary / #parentSources.
        // Assert that concrete structure SCOPED to the modal (not generic
        // page text), so a wrong page would fail this test.  We do not depend
        // on #parentContext being *visible*: it is populated by the
        // /api/followup/prepare DB lookup, which returns 404 for the synthetic
        // id (no such research) and leaves the block hidden — so we assert only
        // on the structural reference existing in the modal.
        const result = await page.evaluate(() => {
            const modal = document.getElementById('followUpModal');
            if (!modal) return { hasModal: false };

            const parentCtx = modal.querySelector('#parentContext');
            const parentSummary = modal.querySelector('#parentSummary');
            const parentSources = modal.querySelector('#parentSources');

            const visibleText = (modal.textContent || '').toLowerCase();
            const mentionsPrevious = visibleText.includes('previous research');

            return {
                hasModal: true,
                hasParentContext: !!parentCtx,
                hasParentSummary: !!parentSummary,
                hasParentSources: !!parentSources,
                mentionsPrevious
            };
        });

        if (!result.hasModal) {
            return { passed: false, message: 'Wrong page loaded: #followUpModal missing' };
        }

        // Real assertion: the parent-research reference structure exists in
        // the modal AND the modal copy explicitly mentions the previous
        // research it builds on.
        const passed = result.hasParentContext &&
                       result.hasParentSummary &&
                       result.hasParentSources &&
                       result.mentionsPrevious;

        return {
            passed,
            message: passed
                ? 'Follow-up modal references parent research (#parentContext/#parentSummary/#parentSources + "previous research" copy)'
                : `Parent-research reference incomplete: parentContext=${result.hasParentContext}, parentSummary=${result.hasParentSummary}, parentSources=${result.hasParentSources}, mentionsPrevious=${result.mentionsPrevious}`
        };
    }
};

// ============================================================================
// Follow-up API Tests
// ============================================================================
const FollowupApiTests = {
    async followupApiEndpointExists(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(async (url) => {
            try {
                // Test OPTIONS or GET to see if endpoint exists
                const response = await fetch(`${url}/followup/prepare`, {
                    method: 'OPTIONS'
                });

                // Even 405 (Method Not Allowed) means endpoint exists
                return {
                    exists: response.status !== 404,
                    status: response.status
                };
            } catch {
                // Try GET
                try {
                    const getResponse = await fetch(`${url}/followup/prepare`);
                    return {
                        exists: getResponse.status !== 404,
                        status: getResponse.status
                    };
                } catch (e2) {
                    return { exists: false, error: e2.message };
                }
            }
        }, baseUrl);

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'Follow-up API endpoint not found' };
        }

        return {
            passed: true,
            message: `Follow-up API endpoint exists (status: ${result.status})`
        };
    }
};

// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
    log.section('Follow-up Research Tests');

    const ctx = await setupTest({ authenticate: true });
    const results = new TestResults('Follow-up Research Tests');
    const { page } = ctx;
    const { baseUrl } = ctx.config;

    const subTestTimeout = ctx.config.isCI ? 60000 : 30000;
    async function run(category, name, testFn) {
        try {
            const result = await withTimeout(
                testFn(page, baseUrl),
                subTestTimeout,
                `${category}/${name}`
            );
            if (result.skipped) {
                results.skip(category, name, result.message);
            } else {
                results.add(category, name, result.passed, result.message);
            }
        } catch (error) {
            results.add(category, name, false, `Error: ${error.message}`);
            // If a test timed out, the page may be in a broken state (e.g.
            // a pending navigation that partially completed).  Navigate to
            // about:blank so subsequent tests don't hit "detached frame"
            // errors and can start fresh.
            try {
                await page.goto('about:blank', { timeout: 5000 });
            } catch {
                // Best-effort recovery — don't mask the original failure.
            }
        }
    }

    try {
        // Follow-up Research Tests
        log.section('Follow-up Research');

        await run('Followup', 'Follow-up Button On Results', (p, u) => FollowupResearchTests.followupButtonOnResults(p, u));
        await run('Followup', 'Follow-up Modal Opens', (p, u) => FollowupResearchTests.followupModalOpens(p, u));
        await run('Followup', 'Follow-up Query Prefilled', (p, u) => FollowupResearchTests.followupQueryPrefilled(p, u));
        await run('Followup', 'Follow-up Submit Button', (p, u) => FollowupResearchTests.followupSubmitButton(p, u));
        await run('Followup', 'Follow-up Mode Selection', (p, u) => FollowupResearchTests.followupModeSelection(p, u));
        await run('Followup', 'Follow-up Links To Original', (p, u) => FollowupResearchTests.followupLinksToOriginal(p, u));

        // API Tests
        log.section('Follow-up API');
        await run('API', 'Follow-up API Endpoint Exists', (p, u) => FollowupApiTests.followupApiEndpointExists(p, u));

    } catch (error) {
        log.error(`Fatal error: ${error.message}`);
        console.error(error.stack);
    } finally {
        results.print();
        results.save();
        await teardownTest(ctx);
        process.exit(results.exitCode());
    }
}

// Run if executed directly
if (require.main === module) {
    main().catch(error => {
        console.error('Test runner failed:', error);
        process.exit(1);
    });
}

module.exports = { FollowupResearchTests, FollowupApiTests };
