// End-to-end cascade behaviour for the archive browser.
//
// Pins the contract for how the three filter layers compose:
//   1. Folder-type bar    (enabledFolderTypes ⊂ {issued, received, mdl, incoming})
//   2. Selected parties   (selectedGroupingFolders, name-keyed)
//   3. Selected projects  (visibleProjects, in multi-project mode)
//
// Specifically targets the symptom "I select various third-party folders and
// the Issued/Received/MDL/Incoming badges, and sometimes files that should be
// there aren't shown". Each test asserts both the file table and the
// transmittal folder list, since the user reported both can drop entries.

import { test, expect } from '@playwright/test';
import { MOCK_FS_INIT_SCRIPT } from './fixtures/mock-fs-api.js';
import * as path from 'path';

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

// All four folder types enabled — used at the start of each test so default
// (issued+received only) isn't quietly hiding things.
async function enableAllFolderTypes(page) {
    await page.evaluate(() => {
        window.app.enabledFolderTypes = new Set(['issued', 'received', 'mdl', 'incoming']);
    });
}

async function selectAllParties(page) {
    await page.evaluate(() => {
        const cb = document.getElementById('selectAllGroupingCheckbox');
        if (cb && !cb.checked) cb.click();
    });
    await page.waitForTimeout(200);
}

// Returns the names of files currently visible in the table (parsed from the
// row labels). Folder-type changes flow through applyFilters → filteredFiles
// → updateFileTable, so reading filteredFiles directly is the closest signal.
async function visibleFileNames(page) {
    return page.evaluate(() => window.app.filteredFiles.map(f => f.name));
}

// Returns the visible-transmittal-folder paths (the ones that pass the
// grouping/folder-type cascade — what the left panel actually renders).
async function visibleTransmittalPaths(page) {
    return page.evaluate(() => {
        const items = document.querySelectorAll('#transmittalFoldersList .folder-item');
        return Array.from(items).map(el => el.getAttribute('data-path'));
    });
}

test.describe('Archive cascade: folder-type × parties × outstanding', () => {
    test.beforeEach(async ({ page }) => {
        await page.addInitScript(MOCK_FS_INIT_SCRIPT);
    });

    test('all four folder-type badges hide their subtrees and only their subtrees', async ({ page }) => {
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
        await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });

        // Start with all four enabled so the scan picks up MDL/Incoming.
        await enableAllFolderTypes(page);

        await page.evaluate(() => {
            window.__setMockDirectoryTree('Archive', {
                'BM': {
                    'Issued': {
                        '2025-01-01_T-ISSUED (IFC) - I': {
                            '100-EL-SPC-0001_A (IFC) - SpecIssued.pdf': '%PDF',
                        },
                    },
                    'Received': {
                        '2025-01-02_T-RECEIVED (IFC) - R': {
                            '100-EL-SPC-0002_A (IFC) - SpecReceived.pdf': '%PDF',
                        },
                    },
                    'MDL': {
                        '2025-01-03_T-MDL (IFC) - M': {
                            '100-EL-SPC-0003_A (IFC) - SpecMDL.pdf': '%PDF',
                        },
                    },
                    'Incoming': {
                        '2025-01-04_T-INCOMING (IFC) - In': {
                            '100-EL-SPC-0004_A (IFC) - SpecIncoming.pdf': '%PDF',
                        },
                    },
                },
            });
        });

        await page.locator('#addDirectoryBtn').click();
        await page.waitForTimeout(2000);

        // All four folder types should have surfaced files at scan time.
        const allFour = await visibleFileNames(page);
        for (const expected of ['SpecIssued', 'SpecReceived', 'SpecMDL', 'SpecIncoming']) {
            expect(allFour.some(n => n.includes(expected)), `expected ${expected} present with all four enabled; got ${allFour.join(', ')}`).toBe(true);
        }

        // Toggle each folder type off and verify its subtree's files disappear,
        // while the other three stay. This is the load-bearing claim.
        const types = [
            { type: 'issued', expectGone: 'SpecIssued' },
            { type: 'received', expectGone: 'SpecReceived' },
            { type: 'mdl', expectGone: 'SpecMDL' },
            { type: 'incoming', expectGone: 'SpecIncoming' },
        ];

        for (const { type, expectGone } of types) {
            // Disable the type via the canonical app entry point.
            await page.evaluate((t) => window.app.modules.app.toggleFolderType(t), type);
            await page.waitForTimeout(300);

            const visible = await visibleFileNames(page);
            expect(
                visible.some(n => n.includes(expectGone)),
                `disabling ${type} should hide ${expectGone}; saw: ${visible.join(', ')}`
            ).toBe(false);

            // The other three subtrees keep their files.
            for (const other of types) {
                if (other.type === type) continue;
                expect(
                    visible.some(n => n.includes(other.expectGone)),
                    `disabling ${type} must not hide ${other.expectGone}; saw: ${visible.join(', ')}`
                ).toBe(true);
            }

            // Re-enable for the next iteration. toggleFolderType triggers a
            // refreshDirectories() because the scan dropped that subtree —
            // we need to wait for the rescan to finish before continuing.
            await page.evaluate((t) => window.app.modules.app.toggleFolderType(t), type);
            await page.waitForTimeout(2000);
        }
    });

    test('toggling a folder type off then on does not duplicate or lose files', async ({ page }) => {
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
        await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });

        await enableAllFolderTypes(page);

        await page.evaluate(() => {
            window.__setMockDirectoryTree('Archive', {
                'BM': {
                    'Issued': {
                        '2025-01-01_T-ISSUED (IFC) - I': {
                            '100-EL-SPC-0001_A (IFC) - I1.pdf': '%PDF',
                            '100-EL-SPC-0001_B (IFC) - I2.pdf': '%PDF',
                        },
                    },
                    'Received': {
                        '2025-01-02_T-RECEIVED (IFC) - R': {
                            '100-EL-SPC-0002_A (IFC) - R1.pdf': '%PDF',
                        },
                    },
                },
            });
        });

        await page.locator('#addDirectoryBtn').click();
        await page.waitForTimeout(2000);

        const baseline = await page.evaluate(() => ({
            files: window.app.files.length,
            txn: window.app.transmittalFolders.length,
            grouping: window.app.groupingFolders.length,
        }));
        expect(baseline.files).toBe(3);

        // Toggle Issued off → on.
        await page.evaluate(() => window.app.modules.app.toggleFolderType('issued'));
        await page.waitForTimeout(300);
        await page.evaluate(() => window.app.modules.app.toggleFolderType('issued'));
        await page.waitForTimeout(2000);

        const after = await page.evaluate(() => ({
            files: window.app.files.length,
            txn: window.app.transmittalFolders.length,
            grouping: window.app.groupingFolders.length,
            // Distinct-file-id counts to catch duplication via re-scan.
            distinctIds: new Set(window.app.files.map(f => f.id)).size,
            distinctPaths: new Set(window.app.files.map(f => f.path)).size,
        }));

        expect(after.files, 'no file duplication after toggle off→on').toBe(baseline.files);
        expect(after.distinctIds, 'each file should still have a unique id').toBe(after.files);
        expect(after.distinctPaths, 'no duplicate file paths').toBe(after.files);
        expect(after.grouping, 'grouping folders not duplicated').toBe(baseline.grouping);
        expect(after.txn, 'transmittal folders not duplicated').toBe(baseline.txn);
    });

    test('outstanding files: visible under <party>/, <party>/Issued/, <party>/MDL/ matches the folder-type cascade', async ({ page }) => {
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
        await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });

        await enableAllFolderTypes(page);

        await page.evaluate(() => {
            window.__setMockDirectoryTree('Archive', {
                'BM': {
                    // Loose file directly under party — not under any folder-type marker.
                    '100-EL-SPC-LOOSE_A (IFC) - LooseAtParty.pdf': '%PDF',
                    'Issued': {
                        // Loose file under Issued (no transmittal folder wrapper)
                        '100-EL-SPC-LOOSE_A (IFC) - LooseAtIssued.pdf': '%PDF',
                    },
                    'MDL': {
                        '100-EL-SPC-LOOSE_A (IFC) - LooseAtMDL.pdf': '%PDF',
                    },
                },
            });
        });

        await page.locator('#addDirectoryBtn').click();
        await page.waitForTimeout(2000);

        await selectAllParties(page);

        // With all folder types enabled, all three loose files should be visible
        // under the Outstanding virtual transmittal.
        const allEnabled = await visibleFileNames(page);
        expect(allEnabled.some(n => n.includes('LooseAtParty'))).toBe(true);
        expect(allEnabled.some(n => n.includes('LooseAtIssued'))).toBe(true);
        expect(allEnabled.some(n => n.includes('LooseAtMDL'))).toBe(true);

        // Disable MDL — only LooseAtMDL should drop.
        await page.evaluate(() => window.app.modules.app.toggleFolderType('mdl'));
        await page.waitForTimeout(2000);

        const noMDL = await visibleFileNames(page);
        expect(noMDL.some(n => n.includes('LooseAtParty'))).toBe(true);
        expect(noMDL.some(n => n.includes('LooseAtIssued'))).toBe(true);
        expect(noMDL.some(n => n.includes('LooseAtMDL'))).toBe(false);

        // Disable Issued too — LooseAtIssued drops, LooseAtParty stays
        // (LooseAtParty's path has no folder-type segment at all).
        await page.evaluate(() => window.app.modules.app.toggleFolderType('issued'));
        await page.waitForTimeout(2000);

        const noIssuedMDL = await visibleFileNames(page);
        expect(noIssuedMDL.some(n => n.includes('LooseAtParty'))).toBe(true);
        expect(noIssuedMDL.some(n => n.includes('LooseAtIssued'))).toBe(false);
        expect(noIssuedMDL.some(n => n.includes('LooseAtMDL'))).toBe(false);
    });

    test('same-name party across two projects + folder-type cascade hides both projects symmetrically', async ({ page }) => {
        // BM in ProjectA AND ProjectB, each with Issued and Received subtrees.
        // Disabling Received must hide BOTH projects' Received files;
        // selecting BM (one row in the panel) must surface both projects' Issued.
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
        await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });

        await enableAllFolderTypes(page);

        await page.evaluate(() => {
            window.__setMockDirectoryTree('combined-root', {
                'ProjectA': {
                    'Archive': {
                        'BM': {
                            'Issued': {
                                '2025-01-01_T-A-I (IFC) - X': {
                                    '100-EL-SPC-0001_A (IFC) - A_Issued.pdf': '%PDF',
                                },
                            },
                            'Received': {
                                '2025-01-02_T-A-R (IFC) - X': {
                                    '100-EL-SPC-0002_A (IFC) - A_Received.pdf': '%PDF',
                                },
                            },
                        },
                    },
                },
                'ProjectB': {
                    'Archive': {
                        'BM': {
                            'Issued': {
                                '2025-02-01_T-B-I (IFC) - X': {
                                    '200-EL-SPC-0001_A (IFC) - B_Issued.pdf': '%PDF',
                                },
                            },
                            'Received': {
                                '2025-02-02_T-B-R (IFC) - X': {
                                    '200-EL-SPC-0002_A (IFC) - B_Received.pdf': '%PDF',
                                },
                            },
                        },
                    },
                },
            });
            window.app.projectFilter = new Set(['ProjectA', 'ProjectB']);
            window.app.visibleProjects = new Set(['ProjectA', 'ProjectB']);
            window.app.isMultiProject = true;
        });

        await page.locator('#addDirectoryBtn').click();
        await page.waitForTimeout(2000);

        // BM should appear once in the parties panel.
        const partyRows = await page.locator('#groupingFoldersList .folder-item-name').allTextContents();
        const bmCount = partyRows.filter(n => n.trim() === 'BM').length;
        expect(bmCount, 'BM merged to one party row across projects').toBe(1);

        // All four files visible with all four folder types enabled.
        const all = await visibleFileNames(page);
        expect(all.some(n => n.includes('A_Issued'))).toBe(true);
        expect(all.some(n => n.includes('B_Issued'))).toBe(true);
        expect(all.some(n => n.includes('A_Received'))).toBe(true);
        expect(all.some(n => n.includes('B_Received'))).toBe(true);

        // Disable Received — BOTH projects' Received files hide.
        await page.evaluate(() => window.app.modules.app.toggleFolderType('received'));
        await page.waitForTimeout(2000);

        const noReceived = await visibleFileNames(page);
        expect(noReceived.some(n => n.includes('A_Issued')), 'ProjectA Issued stays').toBe(true);
        expect(noReceived.some(n => n.includes('B_Issued')), 'ProjectB Issued stays').toBe(true);
        expect(noReceived.some(n => n.includes('A_Received')), 'ProjectA Received hidden').toBe(false);
        expect(noReceived.some(n => n.includes('B_Received')), 'ProjectB Received hidden').toBe(false);

        // Transmittal folder list also drops both projects' Received transmittals.
        const txnPaths = await visibleTransmittalPaths(page);
        expect(txnPaths.every(p => !p.includes('/Received/')), 'no Received transmittals: ' + txnPaths.join(', ')).toBe(true);
    });

    test('toggling a party off then on with selectAllTransmittals re-syncs the transmittal selection', async ({ page }) => {
        // Default selectAllTransmittals=true. After deselecting BM, BM's
        // transmittal folders are gone from the list. Reselecting BM should
        // make them visible AND auto-selected (because select-all is still on).
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
        await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });

        await enableAllFolderTypes(page);

        await page.evaluate(() => {
            window.__setMockDirectoryTree('Archive', {
                'BM': {
                    'Issued': {
                        '2025-01-01_T-BM (IFC) - X': {
                            '100-EL-SPC-0001_A (IFC) - BM1.pdf': '%PDF',
                        },
                    },
                },
                'OTHER': {
                    'Issued': {
                        '2025-01-02_T-OTHER (IFC) - X': {
                            '200-EL-SPC-0002_A (IFC) - OTHER1.pdf': '%PDF',
                        },
                    },
                },
            });
        });

        await page.locator('#addDirectoryBtn').click();
        await page.waitForTimeout(2000);

        // Both parties auto-selected, both transmittals auto-selected.
        const initial = await page.evaluate(() => ({
            selectAllTxn: window.app.selectAllTransmittals,
            selectedGrouping: Array.from(window.app.selectedGroupingFolders).sort(),
            selectedTxn: Array.from(window.app.selectedTransmittalFolders).sort(),
        }));
        expect(initial.selectAllTxn).toBe(true);
        expect(initial.selectedGrouping).toEqual(['BM', 'OTHER']);
        expect(initial.selectedTxn.some(p => p.includes('T-BM'))).toBe(true);
        expect(initial.selectedTxn.some(p => p.includes('T-OTHER'))).toBe(true);

        // Deselect BM via the canonical event handler (mimics user click)
        await page.evaluate(() => {
            window.app.selectAllGroupingFolders = false;
            window.app.selectedGroupingFolders.delete('BM');
            window.app.modules.app.renderGroupingFolders();
            window.app.modules.app.renderTransmittalFolders();
            window.app.modules.filtering.applyFilters();
        });
        await page.waitForTimeout(200);

        // BM's transmittal is no longer visible AND no longer in the selection set.
        const afterDeselect = await page.evaluate(() => ({
            selectedTxn: Array.from(window.app.selectedTransmittalFolders).sort(),
            visiblePaths: Array.from(document.querySelectorAll('#transmittalFoldersList .folder-item'))
                .map(el => el.getAttribute('data-path')),
        }));
        expect(afterDeselect.selectedTxn.some(p => p.includes('T-BM'))).toBe(false);
        expect(afterDeselect.visiblePaths.some(p => p && p.includes('T-BM'))).toBe(false);

        // Re-add BM. Because selectAllTransmittals stays true, BM's
        // transmittal should be auto-selected on the re-render.
        await page.evaluate(() => {
            window.app.selectedGroupingFolders.add('BM');
            window.app.modules.app.renderGroupingFolders();
            window.app.modules.app.renderTransmittalFolders();
            window.app.modules.filtering.applyFilters();
        });
        await page.waitForTimeout(200);

        const afterReselect = await page.evaluate(() => ({
            selectAllTxn: window.app.selectAllTransmittals,
            selectedTxn: Array.from(window.app.selectedTransmittalFolders).sort(),
            visibleFiles: window.app.filteredFiles.map(f => f.name),
        }));
        expect(afterReselect.selectAllTxn).toBe(true);
        expect(
            afterReselect.selectedTxn.some(p => p.includes('T-BM')),
            'BM transmittal should be re-selected via selectAll cascade'
        ).toBe(true);
        expect(afterReselect.visibleFiles.some(n => n.includes('BM1'))).toBe(true);
    });
});

test.describe('Archive cascade: nested-party path-segment contract', () => {
    test.beforeEach(async ({ page }) => {
        await page.addInitScript(MOCK_FS_INIT_SCRIPT);
    });

    // Pin the contract for nested-party paths. With folder-type segment NOT
    // immediately after the party, the current rule (transmittalIsUnderVisibleParty
    // checks only the segment right after the first matched party) means the
    // folder-type cascade can leak. Either the test should fail and we fix
    // the cascade to walk all party matches; or the contract is "the folder
    // type only applies one level under the party". Recording the call here
    // forces us to face the question explicitly when a future change touches
    // the helper.
    test('nested party + deep folder-type marker: the deep folder-type filter applies to the file', async ({ page }) => {
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
        await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });

        await enableAllFolderTypes(page);

        await page.evaluate(() => {
            window.__setMockDirectoryTree('Archive', {
                'BM': {
                    'sub': {
                        'Issued': {
                            '2025-01-01_T-NESTED (IFC) - X': {
                                '100-EL-SPC-NESTED_A (IFC) - SpecDeepIssued.pdf': '%PDF',
                            },
                        },
                    },
                },
            });
        });

        await page.locator('#addDirectoryBtn').click();
        await page.waitForTimeout(2000);

        await selectAllParties(page);

        // With Issued enabled, the file is visible.
        const visibleAll = await visibleFileNames(page);
        expect(visibleAll.some(n => n.includes('SpecDeepIssued'))).toBe(true);

        // Turn Issued OFF — the file lives under .../Issued/... so its scan-time
        // listing should be skipped. After the toggle-driven rescan, the file
        // must NOT be visible regardless of how transmittalIsUnderVisibleParty
        // walks segments.
        await page.evaluate(() => window.app.modules.app.toggleFolderType('issued'));
        await page.waitForTimeout(2000);

        const visibleNoIssued = await visibleFileNames(page);
        expect(
            visibleNoIssued.some(n => n.includes('SpecDeepIssued')),
            'deep Issued subtree must hide when Issued is toggled off'
        ).toBe(false);
    });
});

test.describe('Archive cascade: URL state round-trip', () => {
    test.beforeEach(async ({ page }) => {
        await page.addInitScript(MOCK_FS_INIT_SCRIPT);
    });

    test('non-default folder-type set round-trips through serialize → restore', async ({ page }) => {
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
        await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });

        const qs = await page.evaluate(() => {
            window.app.enabledFolderTypes = new Set(['issued', 'mdl']); // received off, incoming off, mdl on
            return window.app.modules.urlState.serialize();
        });
        // serialize() returns "?types=..." (leading "?" included) or "" when no
        // diffs from defaults exist. Strip the leading "?" before re-emitting.
        const cleanQs = qs.startsWith('?') ? qs.slice(1) : qs;
        expect(cleanQs).toContain('types=');

        const round = await page.evaluate((querystring) => {
            // Reset to defaults so restore has work to do.
            window.app.enabledFolderTypes = new Set(['issued', 'received']);
            history.replaceState(null, '', '?' + querystring);
            window.app.modules.urlState.restore();
            return Array.from(window.app.enabledFolderTypes).sort();
        }, cleanQs);

        expect(round).toEqual(['issued', 'mdl']);
    });

    // Selected parties are deliberately NOT in URL state today — the natural
    // flow is: pick a directory locally, then narrow with party checkboxes.
    // Pin this in a test so accidentally adding party serialization later
    // doesn't break sharing semantics; remove the test (and the assertion)
    // when/if sharing-with-parties becomes a feature.
    test('selected parties are NOT serialized to URL state (current contract)', async ({ page }) => {
        await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
        await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });

        const qs = await page.evaluate(() => {
            window.app.selectAllGroupingFolders = false;
            window.app.selectedGroupingFolders = new Set(['BM', 'OTHER']);
            return window.app.modules.urlState.serialize();
        });
        expect(qs).not.toContain('parties=');
        expect(qs).not.toContain('groups=');
        expect(qs).not.toContain('selected=');
    });
});
