// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { assert } from 'chai';
import sinon from 'sinon';
import * as SDK from '../../core/sdk/sdk.js';
import { findMenuItemWithLabel } from '../../testing/ContextMenuHelpers.js';
import { createTarget } from '../../testing/EnvironmentHelpers.js';
import { describeWithMockConnection } from '../../testing/MockConnection.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Application from './application.js';
describeWithMockConnection('DeviceBoundSessionsTreeElement', () => {
    let model;
    let target;
    let mockPanel;
    function makeSession(site, sessionId) {
        return {
            key: { site, id: sessionId },
            refreshUrl: 'https://example1.com/refresh',
            inclusionRules: { origin: 'https://example1.com', includeSite: true, urlRules: [] },
            cookieCravings: [],
            expiryDate: 1767225600,
            allowedRefreshInitiators: [],
        };
    }
    beforeEach(() => {
        target = createTarget();
        const networkManager = target.model(SDK.NetworkManager.NetworkManager);
        model = new Application.DeviceBoundSessionsModel.DeviceBoundSessionsModel();
        model.modelAdded(networkManager);
        mockPanel = {};
    });
    it('builds the correct tree hierarchy on INITIALIZE_SESSIONS event only for visible sites', () => {
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        model.addVisibleSite('example1.com');
        model.addVisibleSite('example2.com');
        const sessions = [
            makeSession('example1.com', 'session_1'),
            makeSession('example1.com', 'session_2'),
            makeSession('example2.com', 'session_3'),
            makeSession('hidden.com', 'session_4'),
        ];
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions });
        // Verify site nodes.
        assert.strictEqual(root.childCount(), 2);
        const site1Node = root.children()[0];
        const site2Node = root.children()[1];
        assert.strictEqual(site1Node.title, 'example1.com');
        assert.strictEqual(site2Node.title, 'example2.com');
        // Verify session nodes under example1.com.
        assert.strictEqual(site1Node.childCount(), 2);
        assert.strictEqual(site1Node.children()[0].title, 'session_1');
        assert.strictEqual(site1Node.children()[1].title, 'session_2');
        // Verify session nodes under example2.com.
        assert.strictEqual(site2Node.childCount(), 1);
        assert.strictEqual(site2Node.children()[0].title, 'session_3');
    });
    it('adds a tree element when a site becomes visible after data is already loaded', () => {
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        const sessions = [
            makeSession('example1.com', 'session_1'),
            makeSession('example2.com', 'session_1'),
            makeSession('example2.com', 'session_2'),
            makeSession('hidden.com', 'session_3'),
        ];
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions });
        assert.strictEqual(root.childCount(), 0);
        model.addVisibleSite('example1.com');
        model.addVisibleSite('example2.com');
        assert.strictEqual(root.childCount(), 2);
        const siteNode1 = root.children()[0];
        assert.strictEqual(siteNode1.title, 'example1.com');
        assert.strictEqual(siteNode1.childCount(), 1);
        assert.strictEqual(siteNode1.children()[0].title, 'session_1');
        const siteNode2 = root.children()[1];
        assert.strictEqual(siteNode2.title, 'example2.com');
        assert.strictEqual(siteNode2.childCount(), 2);
        assert.strictEqual(siteNode2.children()[0].title, 'session_1');
        assert.strictEqual(siteNode2.children()[1].title, 'session_2');
    });
    it('removes empty tree elements on CLEAR_EVENTS', () => {
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        model.addVisibleSite('example1.com');
        model.addVisibleSite('example2.com');
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'example1.com', sessionId: undefined });
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'example1.com', sessionId: 'session_1' });
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'example1.com', sessionId: 'session_2' });
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'example2.com', sessionId: 'session_1' });
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'hidden.com', sessionId: 'session_1' });
        assert.strictEqual(root.childCount(), 2);
        const siteNode1 = root.children()[0];
        const siteNode2 = root.children()[1];
        assert.strictEqual(siteNode1.title, 'example1.com');
        assert.strictEqual(siteNode2.title, 'example2.com');
        assert.strictEqual(siteNode1.childCount(), 3);
        assert.strictEqual(siteNode2.childCount(), 1);
        assert.strictEqual(siteNode1.children()[0].title, 'No session');
        assert.strictEqual(siteNode1.children()[1].title, 'session_1');
        assert.strictEqual(siteNode1.children()[2].title, 'session_2');
        assert.strictEqual(siteNode2.children()[0].title, 'session_1');
        model.dispatchEventToListeners("CLEAR_EVENTS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.CLEAR_EVENTS */, {
            emptySessions: new Map([
                ['example1.com', [undefined, 'session_1']],
                ['example2.com', ['session_1']],
                ['hidden.com', ['session_1']],
            ]),
            emptySites: new Set(['example2.com', 'hidden.com']),
            noLongerFailedSessions: new Map(),
        });
        model.addVisibleSite('hidden.com');
        assert.strictEqual(root.childCount(), 1);
        const siteNode = root.children()[0];
        assert.strictEqual(siteNode.title, 'example1.com');
        assert.strictEqual(siteNode.children()[0].title, 'session_2');
    });
    it('removes a tree element if visible sites are cleared', () => {
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        model.addVisibleSite('example.com');
        const session = makeSession('example.com', 'session_1');
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session] });
        assert.strictEqual(root.childCount(), 1);
        model.clearVisibleSites();
        assert.strictEqual(root.childCount(), 0);
    });
    it('does not duplicate sites or sessions if data is re-sent', () => {
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        model.addVisibleSite('example1.com');
        const session = makeSession('example1.com', 'session_1');
        // First event.
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session] });
        assert.strictEqual(root.childCount(), 1);
        assert.strictEqual(root.children()[0].childCount(), 1);
        // Second event.
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session] });
        assert.strictEqual(root.childCount(), 1);
        assert.strictEqual(root.children()[0].childCount(), 1);
    });
    it('shows the details view when a session tree element is selected', () => {
        const showSessionSpy = sinon.spy();
        mockPanel.showDeviceBoundSession = showSessionSpy;
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        model.addVisibleSite('example.com');
        const session = makeSession('example.com', 'session-123');
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session] });
        assert.strictEqual(root.childCount(), 1);
        const siteNode = root.children()[0];
        assert.strictEqual(siteNode.childCount(), 1);
        const sessionNode = siteNode.children()[0];
        sessionNode.onselect(false);
        sinon.assert.calledOnce(showSessionSpy);
        sinon.assert.calledWith(showSessionSpy, model, 'example.com', 'session-123');
    });
    it('shows the default view when a site tree element is selected', () => {
        const showDefaultSpy = sinon.spy();
        mockPanel.showDeviceBoundSessionDefault = showDefaultSpy;
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        model.addVisibleSite('example.com');
        const session = makeSession('example.com', 'session-123');
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session] });
        assert.strictEqual(root.childCount(), 1);
        const siteNode = root.children()[0];
        siteNode.onselect(false);
        sinon.assert.calledOnce(showDefaultSpy);
        sinon.assert.calledWith(showDefaultSpy, model, 'Device bound sessions', 'On this page you can view device bound sessions and associated events');
    });
    it('shows the default view when the root tree element is selected', () => {
        const showDefaultSpy = sinon.spy();
        mockPanel.showDeviceBoundSessionDefault = showDefaultSpy;
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        root.onselect(false);
        sinon.assert.calledOnce(showDefaultSpy);
        sinon.assert.calledWith(showDefaultSpy, model, 'Device bound sessions', 'On this page you can view device bound sessions and associated events');
    });
    it('adds a tree element when EVENT_OCCURRED fires for a new session', () => {
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        model.addVisibleSite('example1.com');
        model.addVisibleSite('example2.com');
        assert.strictEqual(root.childCount(), 0);
        // An event occurs that adds a new site + session.
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'example1.com', sessionId: 'session_1' });
        assert.strictEqual(root.childCount(), 1);
        const siteNode1 = root.children()[0];
        assert.strictEqual(siteNode1.title, 'example1.com');
        assert.strictEqual(siteNode1.childCount(), 1);
        assert.strictEqual(siteNode1.children()[0].title, 'session_1');
        // An event occurs that adds a new session to the existing site.
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'example1.com', sessionId: 'session_2' });
        assert.strictEqual(root.childCount(), 1);
        const siteNode2 = root.children()[0];
        assert.strictEqual(siteNode2.title, 'example1.com');
        assert.strictEqual(siteNode2.childCount(), 2);
        assert.strictEqual(siteNode2.children()[0].title, 'session_1');
        assert.strictEqual(siteNode2.children()[1].title, 'session_2');
        // An event occurs that adds a "no session" to the existing site.
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'example1.com' });
        assert.strictEqual(root.childCount(), 1);
        const siteNode3 = root.children()[0];
        assert.strictEqual(siteNode3.title, 'example1.com');
        assert.strictEqual(siteNode3.childCount(), 3);
        assert.strictEqual(siteNode3.children()[0].title, 'No session');
        assert.isTrue(siteNode3.children()[0].listItemElement.classList.contains('no-device-bound-session'));
        assert.strictEqual(siteNode3.children()[1].title, 'session_1');
        assert.isFalse(siteNode3.children()[1].listItemElement.classList.contains('no-device-bound-session'));
        assert.strictEqual(siteNode3.children()[2].title, 'session_2');
        assert.isFalse(siteNode3.children()[2].listItemElement.classList.contains('no-device-bound-session'));
        // An event occurs that adds a "no session" to a new site.
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'example2.com' });
        assert.strictEqual(root.childCount(), 2);
        assert.strictEqual(root.children()[0].title, 'example1.com');
        const siteNode4 = root.children()[1];
        assert.strictEqual(siteNode4.title, 'example2.com');
        assert.strictEqual(siteNode4.childCount(), 1);
        assert.strictEqual(siteNode4.children()[0].title, 'No session');
        assert.isTrue(siteNode4.children()[0].listItemElement.classList.contains('no-device-bound-session'));
        // An event occurs that adds a new session + site but it is not visible.
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site: 'hidden.com', sessionId: 'hidden_session' });
        assert.strictEqual(root.childCount(), 2);
        assert.strictEqual(root.children()[0].title, 'example1.com');
        assert.strictEqual(root.children()[1].title, 'example2.com');
    });
    it('updates the session tree element visual state when a session is terminated', () => {
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        const site = 'example.com';
        const sessionId = 'session_1';
        const otherSessionId = 'session_2';
        model.addVisibleSite(site);
        const session1 = makeSession(site, sessionId);
        const session2 = makeSession(site, otherSessionId);
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session1, session2] });
        const siteNode = root.children()[0];
        const session1Node = siteNode.children()[0];
        const session2Node = siteNode.children()[1];
        // Initially is not terminated.
        assert.isFalse(session1Node.listItemElement.classList.contains('device-bound-session-terminated'));
        assert.isFalse(session2Node.listItemElement.classList.contains('device-bound-session-terminated'));
        // Simulate termination event.
        const isSessionTerminatedStub = sinon.stub(model, 'isSessionTerminated');
        isSessionTerminatedStub.withArgs(site, sessionId).returns(true);
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site, sessionId });
        // Session 1 should now be terminated. Session 2 remains unterminated.
        assert.isTrue(session1Node.listItemElement.classList.contains('device-bound-session-terminated'));
        assert.strictEqual(session1Node.listItemElement.getAttribute('aria-label'), 'session_1, Session terminated');
        assert.isFalse(session2Node.listItemElement.classList.contains('device-bound-session-terminated'));
        assert.strictEqual(session2Node.listItemElement.getAttribute('aria-label'), 'session_2');
        // Simulate recreation event.
        isSessionTerminatedStub.withArgs(site, sessionId).returns(false);
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site, sessionId });
        // Session 1 should no longer be terminated.
        assert.isFalse(session1Node.listItemElement.classList.contains('device-bound-session-terminated'));
        assert.strictEqual(session1Node.listItemElement.getAttribute('aria-label'), 'session_1');
        assert.isFalse(session2Node.listItemElement.classList.contains('device-bound-session-terminated'));
    });
    it('updates the session tree element visual state when a session has errors', () => {
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        const site = 'example.com';
        const sessionId = 'session_1';
        const sessionId2 = 'session_2';
        model.addVisibleSite(site);
        const session = makeSession(site, sessionId);
        const session2 = makeSession(site, sessionId2);
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session, session2] });
        const siteNode = root.children()[0];
        const sessionNode = siteNode.children()[0];
        const sessionNode2 = siteNode.children()[1];
        function checkIcon(node, expectedIcon) {
            const icon = node.listItemElement.querySelector('devtools-icon');
            assert.exists(icon);
            assert.strictEqual(icon.getAttribute('name'), expectedIcon);
        }
        // Initially has database icon.
        checkIcon(sessionNode, 'database');
        assert.strictEqual(sessionNode.listItemElement.getAttribute('aria-label'), 'session_1');
        checkIcon(sessionNode2, 'database');
        assert.strictEqual(sessionNode2.listItemElement.getAttribute('aria-label'), 'session_2');
        // A failed event should change it to a warning icon.
        const sessionHasErrorsStub = sinon.stub(model, 'sessionHasErrors');
        sessionHasErrorsStub.withArgs(site, sessionId).returns(true);
        model.dispatchEventToListeners("EVENT_OCCURRED" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.EVENT_OCCURRED */, { site, sessionId });
        checkIcon(sessionNode, 'warning');
        assert.strictEqual(sessionNode.listItemElement.getAttribute('aria-label'), 'session_1, Session has errors');
        checkIcon(sessionNode2, 'database');
        assert.strictEqual(sessionNode2.listItemElement.getAttribute('aria-label'), 'session_2');
        // Clearing events should change it back to a database icon.
        sessionHasErrorsStub.withArgs(site, sessionId).returns(false);
        model.dispatchEventToListeners("CLEAR_EVENTS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.CLEAR_EVENTS */, {
            emptySessions: new Map(),
            emptySites: new Set(),
            noLongerFailedSessions: new Map([[site, [sessionId]]]),
        });
        checkIcon(sessionNode, 'database');
        assert.strictEqual(sessionNode.listItemElement.getAttribute('aria-label'), 'session_1');
        checkIcon(sessionNode2, 'database');
    });
    ['Delete', 'Backspace'].forEach(key => {
        it(`deletes a session when ${key} key is pressed`, () => {
            const deleteSessionSpy = sinon.spy();
            model.deleteSession = deleteSessionSpy;
            const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
            root.onbind();
            model.addVisibleSite('example.com');
            const session = makeSession('example.com', 'session-123');
            model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session] });
            const siteNode = root.children()[0];
            const sessionNode = siteNode.children()[0];
            // Simulate Delete key press.
            const event = new KeyboardEvent('keydown', { key });
            sessionNode.listItemElement.dispatchEvent(event);
            sinon.assert.calledOnce(deleteSessionSpy);
            sinon.assert.calledWith(deleteSessionSpy, 'example.com', 'session-123');
        });
    });
    it('deletes a session when Clear is selected from context menu', () => {
        const deleteSessionSpy = sinon.spy();
        model.deleteSession = deleteSessionSpy;
        const root = new Application.DeviceBoundSessionsTreeElement.RootTreeElement(mockPanel, model);
        root.onbind();
        model.addVisibleSite('example.com');
        const session = makeSession('example.com', 'session-123');
        model.dispatchEventToListeners("INITIALIZE_SESSIONS" /* Application.DeviceBoundSessionsModel.DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS */, { sessions: [session] });
        const siteNode = root.children()[0];
        const sessionNode = siteNode.children()[0];
        const contextMenuShow = sinon.stub(UI.ContextMenu.ContextMenu.prototype, 'show').resolves();
        // Simulate contextmenu event.
        const event = new MouseEvent('contextmenu');
        sessionNode.listItemElement.dispatchEvent(event);
        sinon.assert.calledOnce(contextMenuShow);
        const contextMenu = contextMenuShow.thisValues[0];
        const clearItem = findMenuItemWithLabel(contextMenu.defaultSection(), 'Clear');
        assert.exists(clearItem);
        contextMenu.invokeHandler(clearItem.id());
        sinon.assert.calledOnce(deleteSessionSpy);
        sinon.assert.calledWith(deleteSessionSpy, 'example.com', 'session-123');
    });
});
//# sourceMappingURL=DeviceBoundSessionsTreeElement.test.js.map