// *****************************************************************************
// Copyright (C) 2026 EclipseSource and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();

import { expect } from 'chai';
import { Container } from '@theia/core/shared/inversify';
import { TerminalManagerTreeModel } from './terminal-manager-tree-model';
import { TerminalManagerTreeTypes } from './terminal-manager-types';
import { TreeImpl, Tree } from '@theia/core/lib/browser/tree/tree';
import { TreeSelectionServiceImpl } from '@theia/core/lib/browser/tree/tree-selection-impl';
import { TreeSelectionService } from '@theia/core/lib/browser/tree/tree-selection';
import { TreeExpansionServiceImpl, TreeExpansionService } from '@theia/core/lib/browser/tree/tree-expansion';
import { TreeNavigationService } from '@theia/core/lib/browser/tree/tree-navigation';
import { TreeFocusServiceImpl, TreeFocusService } from '@theia/core/lib/browser/tree/tree-focus-service';
import { TreeSearch } from '@theia/core/lib/browser/tree/tree-search';
import { FuzzySearch } from '@theia/core/lib/browser/tree/fuzzy-search';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { ILogger, bindContributionProvider } from '@theia/core/lib/common';
import { LabelProvider, LabelProviderContribution } from '@theia/core/lib/browser/label-provider';

describe('TerminalManagerTreeModel', () => {
    let container: Container;
    let model: TerminalManagerTreeModel;

    beforeEach(() => {
        container = new Container({ defaultScope: 'Singleton' });
        container.bind(TreeImpl).toSelf();
        container.bind(Tree).toService(TreeImpl);
        container.bind(TreeSelectionServiceImpl).toSelf();
        container.bind(TreeSelectionService).toService(TreeSelectionServiceImpl);
        container.bind(TreeExpansionServiceImpl).toSelf();
        container.bind(TreeExpansionService).toService(TreeExpansionServiceImpl);
        container.bind(TreeNavigationService).toSelf();
        container.bind(TreeFocusServiceImpl).toSelf();
        container.bind(TreeFocusService).toService(TreeFocusServiceImpl);
        container.bind(TreeSearch).toSelf();
        container.bind(FuzzySearch).toSelf();
        container.bind(MockLogger).toSelf();
        container.bind(ILogger).to(MockLogger);
        bindContributionProvider(container, LabelProviderContribution);
        container.bind(LabelProvider).toSelf().inSingletonScope();
        container.bind(TerminalManagerTreeModel).toSelf().inSingletonScope();

        model = container.get(TerminalManagerTreeModel);
    });

    after(() => {
        disableJSDOM();
    });

    describe('deleteTerminalPage', () => {
        it('should call onDidDeletePage when deleting a page', done => {
            // Add a regular page with a terminal
            const pageId = 'page-1' as TerminalManagerTreeTypes.PageId;
            const groupId = 'group-1' as TerminalManagerTreeTypes.GroupId;
            const terminalKey = 'terminal-1' as TerminalManagerTreeTypes.TerminalKey;

            model.addTerminalPage(terminalKey, groupId, pageId, 'Test Terminal');

            // Listen for the delete event
            model.onDidDeletePage(deletedPageId => {
                expect(deletedPageId).to.equal(pageId);
                done();
            });

            // Delete the page
            model.deleteTerminalPage(pageId);
        });

        it('should allow deleting the tasks page when it has children', done => {
            // Add a terminal to the tasks page
            const groupId = 'group-tasks-1' as TerminalManagerTreeTypes.GroupId;
            const terminalKey = 'terminal-task-1' as TerminalManagerTreeTypes.TerminalKey;

            const tasksPageId = model.getSpecialPageConfig('task')!.pageId;
            model.addTerminalPage(terminalKey, groupId, tasksPageId, 'Task Terminal');

            // Verify tasks page exists before deletion
            const tasksPageBefore = model.getNode(tasksPageId);
            expect(tasksPageBefore).to.not.be.undefined;
            expect(TerminalManagerTreeTypes.isPageNode(tasksPageBefore)).to.be.true;

            // Listen for the delete event to verify the deletion was attempted
            model.onDidDeletePage(deletedPageId => {
                expect(deletedPageId).to.equal(tasksPageId);
                done();
            });

            // Delete the tasks page - this should succeed (not be blocked)
            model.deleteTerminalPage(tasksPageId);
        });

        it('should do nothing when trying to delete a non-existent page', () => {
            const nonExistentPageId = 'page-nonexistent' as TerminalManagerTreeTypes.PageId;

            // Should not throw
            expect(() => model.deleteTerminalPage(nonExistentPageId)).to.not.throw();
        });
    });

    describe('neighbor selection after deletion', () => {
        it('should select a neighbor page when the active page is deleted', async () => {
            const pageId1 = 'page-1' as TerminalManagerTreeTypes.PageId;
            const pageId2 = 'page-2' as TerminalManagerTreeTypes.PageId;
            const groupId1 = 'group-1' as TerminalManagerTreeTypes.GroupId;
            const groupId2 = 'group-2' as TerminalManagerTreeTypes.GroupId;
            const key1 = 'terminal-1' as TerminalManagerTreeTypes.TerminalKey;
            const key2 = 'terminal-2' as TerminalManagerTreeTypes.TerminalKey;

            model.addTerminalPage(key1, groupId1, pageId1, 'Terminal 1');
            model.addTerminalPage(key2, groupId2, pageId2, 'Terminal 2');

            // Simulate selection of page2's terminal so page2 becomes active
            const termNode2 = model.getNode(key2);
            expect(TerminalManagerTreeTypes.isTerminalNode(termNode2)).to.be.true;
            if (TerminalManagerTreeTypes.isTerminalNode(termNode2)) {
                model.handleSelectionChanged(termNode2);
            }
            expect(model.activePageNode?.id).to.equal(pageId2);

            // Delete the active page
            model.deleteTerminalPage(pageId2);
            await model.refresh();

            // The remaining page's terminal should now be selected
            const page1 = model.getNode(pageId1);
            expect(page1).to.not.be.undefined;
            expect(TerminalManagerTreeTypes.isPageNode(page1)).to.be.true;
            expect(model.activePageNode?.id).to.equal(pageId1);
            expect(model.activeTerminalNode?.id).to.equal(key1);
            expect(model.selectedNodes.map(n => n.id)).to.deep.equal([key1]);
        });

        it('should select a neighbor terminal when the active terminal is deleted from a group', async () => {
            const pageId = 'page-1' as TerminalManagerTreeTypes.PageId;
            const groupId = 'group-1' as TerminalManagerTreeTypes.GroupId;
            const key1 = 'terminal-1' as TerminalManagerTreeTypes.TerminalKey;
            const key2 = 'terminal-2' as TerminalManagerTreeTypes.TerminalKey;

            model.addTerminalPage(key1, groupId, pageId, 'Terminal 1');
            model.addTerminal(key2, groupId, 'Terminal 2');

            // Ensure the tree index is up-to-date after addTerminal
            await model.refresh();

            // Select the second terminal
            const termNode2 = model.getNode(key2);
            expect(TerminalManagerTreeTypes.isTerminalNode(termNode2)).to.be.true;
            if (TerminalManagerTreeTypes.isTerminalNode(termNode2)) {
                model.handleSelectionChanged(termNode2);
            }
            expect(model.activeTerminalNode?.id).to.equal(key2);

            // Delete the active terminal
            model.deleteTerminalNode(key2);
            await model.refresh();

            // The remaining terminal should still be accessible and selected
            const remaining = model.getNode(key1);
            expect(remaining).to.not.be.undefined;
            expect(TerminalManagerTreeTypes.isTerminalNode(remaining)).to.be.true;
            expect(model.activeTerminalNode?.id).to.equal(key1);
            expect(model.selectedNodes.map(n => n.id)).to.deep.equal([key1]);
        });
    });

    describe('addTerminalPage with special page', () => {
        it('should create special page on first use', () => {
            const tasksPageId = model.getSpecialPageConfig('task')!.pageId;
            const groupId = 'group-1' as TerminalManagerTreeTypes.GroupId;
            const terminalKey = 'terminal-1' as TerminalManagerTreeTypes.TerminalKey;

            model.addTerminalPage(terminalKey, groupId, tasksPageId, 'Task Terminal');

            const page = model.getNode(tasksPageId);
            expect(page).to.not.be.undefined;
            expect(TerminalManagerTreeTypes.isPageNode(page)).to.be.true;
        });

        it('should reuse existing special page', () => {
            const tasksPageId = model.getSpecialPageConfig('task')!.pageId;
            const groupId1 = 'group-1' as TerminalManagerTreeTypes.GroupId;
            const groupId2 = 'group-2' as TerminalManagerTreeTypes.GroupId;
            const key1 = 'terminal-1' as TerminalManagerTreeTypes.TerminalKey;
            const key2 = 'terminal-2' as TerminalManagerTreeTypes.TerminalKey;

            model.addTerminalPage(key1, groupId1, tasksPageId, 'Task 1');
            model.addTerminalPage(key2, groupId2, tasksPageId, 'Task 2');

            const page = model.getNode(tasksPageId);
            expect(TerminalManagerTreeTypes.isPageNode(page)).to.be.true;
            if (TerminalManagerTreeTypes.isPageNode(page)) {
                expect(page.children).to.have.lengthOf(2);
            }
        });

        it('should recreate a special page after its last terminal is removed', () => {
            const tasksPageId = model.getSpecialPageConfig('task')!.pageId;
            const firstGroup = 'group-tasks-a' as TerminalManagerTreeTypes.GroupId;
            const firstTerminal = 'terminal-task-a' as TerminalManagerTreeTypes.TerminalKey;
            const secondGroup = 'group-tasks-b' as TerminalManagerTreeTypes.GroupId;
            const secondTerminal = 'terminal-task-b' as TerminalManagerTreeTypes.TerminalKey;

            // Run a task -> Tasks page, group, and terminal appear.
            model.addTerminalPage(firstTerminal, firstGroup, tasksPageId, 'Task A');
            expect(model.getNode(tasksPageId)).to.not.be.undefined;

            // Remove the terminal -> cascades up and detaches the Tasks page.
            const addPageEvents: TerminalManagerTreeTypes.PageId[] = [];
            model.onDidAddPage(({ pageId }) => addPageEvents.push(pageId));
            model.deleteTerminalNode(firstTerminal);

            // The Tasks page should be fully cleared from the tree index.
            expect(model.getNode(tasksPageId)).to.be.undefined;
            expect(model.getNode(firstGroup)).to.be.undefined;
            expect(model.getNode(firstTerminal)).to.be.undefined;

            // Run another task -> Tasks page should be re-created and re-attached,
            // emitting onDidAddPage so the widget can rebuild the page panel.
            model.addTerminalPage(secondTerminal, secondGroup, tasksPageId, 'Task B');

            expect(addPageEvents).to.deep.equal([tasksPageId]);
            const reattached = model.getNode(tasksPageId);
            expect(TerminalManagerTreeTypes.isPageNode(reattached)).to.be.true;
            if (TerminalManagerTreeTypes.isPageNode(reattached)) {
                expect(reattached.parent).to.equal(model.root);
                expect(reattached.children).to.have.lengthOf(1);
                expect(reattached.children[0].id).to.equal(secondGroup);
            }
        });
    });
});
