// Copyright 2025 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 { renderElementIntoDOM } from '../../testing/DOMHelpers.js';
import * as Console from './console.js';
describe('ConsoleViewport', () => {
    let viewport;
    let mockProvider;
    const viewportHeight = 200; // Matching layout test
    const viewportWidth = 600; // Matching layout test
    class MockProvider {
        itemHeights = [];
        minimumRowHeightValue = 16; // Default from ConsoleViewMessage
        fastHeight(index) {
            return this.itemHeights[index] ?? this.minimumRowHeightValue;
        }
        itemCount() {
            return this.itemHeights.length;
        }
        minimumRowHeight() {
            return this.minimumRowHeightValue;
        }
        itemElement(index) {
            // For these tests, we only need the element's offsetHeight.
            const mockElement = document.createElement('div');
            mockElement.style.height = `${this.itemHeights[index] ?? this.minimumRowHeightValue}px`;
            Object.defineProperty(mockElement, 'offsetHeight', { value: this.itemHeights[index] ?? this.minimumRowHeightValue, configurable: true });
            // Mock the ConsoleViewportElement interface methods
            return {
                willHide: () => { },
                wasShown: () => { },
                element: () => mockElement,
                focusLastChildOrSelf: () => { },
            };
        }
        setItems(heights) {
            this.itemHeights = heights;
        }
    }
    beforeEach(() => {
        mockProvider = new MockProvider();
        viewport = new Console.ConsoleViewport.ConsoleViewport(mockProvider);
        // Simulate setting dimensions like ConsoleTestRunner.fixConsoleViewportDimensions
        viewport.element.style.width = `${viewportWidth}px`;
        viewport.element.style.height = `${viewportHeight}px`;
        // Need to append to DOM for offsetHeight/clientHeight to work
        renderElementIntoDOM(viewport.element);
    });
    /** Helper to set items and refresh the viewport **/
    function setItemsAndRefresh(heights) {
        mockProvider.setItems(heights);
        viewport.invalidate(); // This calls rebuildCumulativeHeights and refresh
    }
    function changeItemHeight(index, newHeight) {
        mockProvider.itemHeights[index] = newHeight;
        // Simulate the element's height changing in the DOM for the viewport's calculations
        const renderedElement = viewport.renderedElementAt(index);
        if (renderedElement) {
            Object.defineProperty(renderedElement, 'offsetHeight', { value: newHeight, configurable: true });
        }
        // Invalidate to rebuild cumulative heights and then refresh
        viewport.invalidate();
    }
    /** Helper to simulate scrolling and refresh **/
    function scrollTo(scrollTop) {
        viewport.element.scrollTop = scrollTop;
        viewport.refresh();
    }
    /** Helper to calculate expected cumulative heights **/
    function calculateCumulativeHeights(itemHeights) {
        const cumulative = new Array(itemHeights.length);
        let currentHeight = 0;
        for (let i = 0; i < itemHeights.length; ++i) {
            currentHeight += itemHeights[i];
            cumulative[i] = currentHeight;
        }
        return cumulative;
    }
    /** Helper to calculate expected visible indices based on scroll and heights **/
    function getExpectedVisibleIndices(scrollTop, viewportHeight, cumulativeHeights) {
        if (cumulativeHeights.length === 0) {
            return { first: -1, last: -1 };
        }
        // Find the first visible index: the first item whose bottom is below scrollTop.
        let first = -1;
        for (let i = 0; i < cumulativeHeights.length; ++i) {
            if (cumulativeHeights[i] > scrollTop) {
                first = i;
                break;
            }
        }
        if (first === -1) {
            // No items are visible from the top.
            return { first: -1, last: -1 };
        }
        // Find the last visible index: the last item whose top is above scrollTop + viewportHeight.
        let last = -1;
        const scrollBottom = scrollTop + viewportHeight;
        for (let i = cumulativeHeights.length - 1; i >= 0; --i) {
            const itemTop = i === 0 ? 0 : cumulativeHeights[i - 1];
            if (itemTop < scrollBottom) {
                last = i;
                break;
            }
        }
        // Ensure the calculated range is valid.
        if (last < first) {
            return { first: -1, last: -1 };
        }
        return { first, last };
    }
    it('should report -1 for first and last visible index when empty', () => {
        setItemsAndRefresh([]);
        assert.strictEqual(viewport.firstVisibleIndex(), -1);
        assert.strictEqual(viewport.lastVisibleIndex(), -1);
    });
    it('should report correct visible indices for uniform height items at the top', () => {
        const itemHeight = 20;
        const itemCount = 100;
        const heights = Array(itemCount).fill(itemHeight);
        setItemsAndRefresh(heights);
        const cumulativeHeights = calculateCumulativeHeights(heights);
        const expected = getExpectedVisibleIndices(0, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
    });
    it('should report correct visible indices for uniform height items when scrolled down', () => {
        const itemHeight = 20;
        const itemCount = 100;
        const heights = Array(itemCount).fill(itemHeight);
        setItemsAndRefresh(heights);
        const scrollTop = 500;
        scrollTo(scrollTop);
        const cumulativeHeights = calculateCumulativeHeights(heights);
        const expected = getExpectedVisibleIndices(scrollTop, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
    });
    it('should report correct visible indices for uniform height items at the bottom', () => {
        const itemHeight = 20;
        const itemCount = 100;
        const heights = Array(itemCount).fill(itemHeight);
        setItemsAndRefresh(heights);
        const cumulativeHeights = calculateCumulativeHeights(heights);
        const totalHeight = cumulativeHeights[cumulativeHeights.length - 1];
        const scrollTop = totalHeight - viewportHeight;
        scrollTo(scrollTop);
        const expected = getExpectedVisibleIndices(scrollTop, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
    });
    it('should report correct visible indices for varying height items at the top', () => {
        const heights = [30, 20, 40, 15, 25, 50, 10, 35, 20, 45, 30, 20, 40, 15, 25, 50, 10, 35, 20, 45]; // 20 items
        setItemsAndRefresh(heights);
        const cumulativeHeights = calculateCumulativeHeights(heights);
        const expected = getExpectedVisibleIndices(0, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
    });
    it('should report correct visible indices for varying height items when scrolled down', () => {
        const heights = [30, 20, 40, 15, 25, 50, 10, 35, 20, 45, 30, 20, 40, 15, 25, 50, 10, 35, 20, 45]; // 20 items
        setItemsAndRefresh(heights);
        const scrollTop = 150;
        scrollTo(scrollTop);
        const cumulativeHeights = calculateCumulativeHeights(heights);
        const expected = getExpectedVisibleIndices(scrollTop, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
    });
    it('should report correct visible indices for varying height items at the bottom', () => {
        const heights = [30, 20, 40, 15, 25, 50, 10, 35, 20, 45, 30, 20, 40, 15, 25, 50, 10, 35, 20, 45]; // 20 items
        setItemsAndRefresh(heights);
        const cumulativeHeights = calculateCumulativeHeights(heights);
        const totalHeight = cumulativeHeights[cumulativeHeights.length - 1];
        const scrollTop = totalHeight - viewportHeight;
        scrollTo(scrollTop);
        const expected = getExpectedVisibleIndices(scrollTop, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
    });
    it('should update cumulative heights when item heights change after initial render', () => {
        const initialHeights = Array(10).fill(40);
        setItemsAndRefresh(initialHeights);
        // Simulate a height change for a rendered item
        const changedIndex = 2; // Must be within the initially rendered range
        const newHeight = 50;
        const updatedHeights = [...initialHeights];
        updatedHeights[changedIndex] = newHeight;
        // Change element's height.
        changeItemHeight(changedIndex, newHeight);
        // Simulate a scroll that would require recalculating heights
        const scrollTop = 100;
        scrollTo(scrollTop); // This should trigger rebuildCumulativeHeightsIfNeeded
        const cumulativeHeights = calculateCumulativeHeights(updatedHeights);
        const expected = getExpectedVisibleIndices(scrollTop, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
    });
    // Test forceScrollItemToBeFirst and forceScrollItemToBeLast
    it('should force an item to be the first visible item', () => {
        const itemHeight = 20;
        const itemCount = 100;
        const heights = Array(itemCount).fill(itemHeight);
        setItemsAndRefresh(heights);
        const targetIndex = 50;
        viewport.forceScrollItemToBeFirst(targetIndex);
        const cumulativeHeights = calculateCumulativeHeights(heights);
        const expectedScrollTop = targetIndex > 0 ? cumulativeHeights[targetIndex - 1] : 0;
        const expected = getExpectedVisibleIndices(expectedScrollTop, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
        assert.strictEqual(viewport.element.scrollTop, expectedScrollTop);
    });
    it('should force an item to be the last visible item', () => {
        const itemHeight = 20;
        const itemCount = 100;
        const heights = Array(itemCount).fill(itemHeight);
        setItemsAndRefresh(heights);
        const targetIndex = 50;
        viewport.forceScrollItemToBeLast(targetIndex);
        const cumulativeHeights = calculateCumulativeHeights(heights);
        const expectedScrollTop = cumulativeHeights[targetIndex] - viewportHeight;
        const expected = getExpectedVisibleIndices(expectedScrollTop, viewportHeight, cumulativeHeights);
        assert.strictEqual(viewport.firstVisibleIndex(), expected.first);
        assert.strictEqual(viewport.lastVisibleIndex(), expected.last);
        assert.strictEqual(viewport.element.scrollTop, expectedScrollTop);
    });
    describe('stick-to-bottom with expanding objects', () => {
        const ITEM_COUNT = 150;
        const INITIAL_ITEM_HEIGHT = 20;
        const EXPANDED_ITEM_HEIGHT_INCREASE = 80; // Makes item 100px tall
        function populateViewport(itemCount, itemHeight) {
            const heights = Array(itemCount).fill(itemHeight);
            setItemsAndRefresh(heights);
        }
        function forceSelectAndFocus(index) {
            viewport.scrollItemIntoView(index);
            const elementToFocus = viewport.renderedElementAt(index);
            if (elementToFocus) {
                elementToFocus.focus();
            }
            // Refresh to ensure onFocusIn logic (like setting virtualSelectedIndex) and updateFocusedItem are processed.
            viewport.refresh();
        }
        function isElementFullyVisible(index) {
            const selectedElement = viewport.renderedElementAt(index);
            if (!selectedElement) {
                return false;
            }
            const selectedRect = selectedElement.getBoundingClientRect();
            const viewportRect = viewport.element.getBoundingClientRect();
            // Using a small tolerance like in the layout test
            const tolerance = 2.5;
            return (selectedRect.top + tolerance >= viewportRect.top && selectedRect.bottom - tolerance <= viewportRect.bottom);
        }
        function getInfo(selectedIndex) {
            viewport.refresh(); // Ensure viewport is up-to-date before getting info
            const isAtBottom = (viewport.element.scrollHeight - viewport.element.scrollTop - viewport.element.clientHeight) < 1;
            const shouldStick = viewport.stickToBottom();
            const selectedVisible = isElementFullyVisible(selectedIndex);
            return { isAtBottom, shouldStick, selectedVisible };
        }
        beforeEach(() => {
            populateViewport(ITEM_COUNT, INITIAL_ITEM_HEIGHT);
        });
        it('keeps last visible object in view when expanded/collapsed if scrolled to bottom', () => {
            // Scroll to bottom
            scrollTo(viewport.element.scrollHeight - viewport.element.clientHeight);
            viewport.setStickToBottom(true); // Explicitly set stick to bottom
            viewport.refresh();
            const lastItemIndex = ITEM_COUNT - 1;
            forceSelectAndFocus(lastItemIndex);
            let info = getInfo(lastItemIndex);
            assert.isTrue(info.isAtBottom, 'Initial: Should be at bottom');
            assert.isTrue(info.shouldStick, 'Initial: Should stick to bottom');
            assert.isTrue(info.selectedVisible, 'Initial: Selected item should be visible');
            // Expand object
            changeItemHeight(lastItemIndex, INITIAL_ITEM_HEIGHT + EXPANDED_ITEM_HEIGHT_INCREASE);
            info = getInfo(lastItemIndex);
            assert.isTrue(info.isAtBottom, 'After expand: Should be at bottom');
            assert.isTrue(info.shouldStick, 'After expand: Should stick to bottom');
            assert.isTrue(info.selectedVisible, 'After expand: Selected item should be visible');
            // Collapse object
            changeItemHeight(lastItemIndex, INITIAL_ITEM_HEIGHT);
            info = getInfo(lastItemIndex);
            assert.isTrue(info.isAtBottom, 'After collapse: Should be at bottom');
            assert.isTrue(info.shouldStick, 'After collapse: Should stick to bottom');
            assert.isTrue(info.selectedVisible, 'After collapse: Selected item should be visible');
        });
        it('keeps first fully visible object in view when expanded/collapsed if not scrolled to bottom', () => {
            // Scroll somewhere in the middle, not at the bottom
            const scrollTop = 100;
            scrollTo(scrollTop); // This also calls refresh
            viewport.setStickToBottom(false); // Explicitly set stick to bottom to false
            viewport.refresh();
            // The layout test uses `viewport.firstVisibleIndex() + 1`.
            const targetIndex = viewport.firstVisibleIndex() + 1;
            assert.isBelow(targetIndex, ITEM_COUNT, 'Target index should be within bounds');
            forceSelectAndFocus(targetIndex);
            let info = getInfo(targetIndex);
            assert.isFalse(info.isAtBottom, 'Initial: Should not be at bottom');
            assert.isFalse(info.shouldStick, 'Initial: Should not stick to bottom');
            assert.isTrue(info.selectedVisible, 'Initial: Selected item should be visible');
            // Expand object
            changeItemHeight(targetIndex, INITIAL_ITEM_HEIGHT + EXPANDED_ITEM_HEIGHT_INCREASE);
            info = getInfo(targetIndex);
            assert.isFalse(info.isAtBottom, 'After expand: Should not be at bottom');
            assert.isFalse(info.shouldStick, 'After expand: Should not stick to bottom');
            assert.isTrue(info.selectedVisible, 'After expand: Selected item should be visible');
            // Collapse object
            changeItemHeight(targetIndex, INITIAL_ITEM_HEIGHT);
            info = getInfo(targetIndex);
            assert.isFalse(info.isAtBottom, 'After collapse: Should not be at bottom');
            assert.isFalse(info.shouldStick, 'After collapse: Should not stick to bottom');
            assert.isTrue(info.selectedVisible, 'After collapse: Selected item should be visible');
        });
    });
});
//# sourceMappingURL=ConsoleViewport.test.js.map