// Copyright 2024 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 * as Lantern from '../lantern.js';
const { BaseNode, NetworkNode } = Lantern.Graph;
function sortedById(nodeArray) {
    return nodeArray.sort((node1, node2) => node1.id.localeCompare(node2.id));
}
/**
 * In the real implementation we pass around Node<T>, which are either CPUNodes
 * or NetworkNodes. Rather than construct those for these tests, which don't
 * need real nodes, we instead create BaseNodes and cast them to satisfy TS.
 */
function makeFakeNode(name) {
    return new BaseNode(name);
}
function createComplexGraph() {
    //   B       F
    //  / \     /
    // A   D - E
    //  \ /     \
    //   C       G - H
    const nodeA = makeFakeNode('A');
    const nodeB = makeFakeNode('B');
    const nodeC = makeFakeNode('C');
    const nodeD = makeFakeNode('D');
    const nodeE = makeFakeNode('E');
    const nodeF = makeFakeNode('F');
    const nodeG = makeFakeNode('G');
    const nodeH = makeFakeNode('H');
    nodeA.addDependent(nodeB);
    nodeA.addDependent(nodeC);
    nodeB.addDependent(nodeD);
    nodeC.addDependent(nodeD);
    nodeD.addDependent(nodeE);
    nodeE.addDependent(nodeF);
    nodeE.addDependent(nodeG);
    nodeG.addDependent(nodeH);
    return {
        nodeA,
        nodeB,
        nodeC,
        nodeD,
        nodeE,
        nodeF,
        nodeG,
        nodeH,
    };
}
describe('BaseNode', () => {
    describe('#constructor', () => {
        it('should set the ID', () => {
            const node = makeFakeNode('foo');
            assert.strictEqual(node.id, 'foo');
        });
    });
    describe('.addDependent', () => {
        it('should add the correct edge', () => {
            const nodeA = makeFakeNode('1');
            const nodeB = makeFakeNode('2');
            nodeA.addDependent(nodeB);
            assert.deepEqual(nodeA.getDependents(), [nodeB]);
            assert.deepEqual(nodeB.getDependencies(), [nodeA]);
        });
    });
    describe('.addDependency', () => {
        it('should add the correct edge', () => {
            const nodeA = makeFakeNode('1');
            const nodeB = makeFakeNode('2');
            nodeA.addDependency(nodeB);
            assert.deepEqual(nodeA.getDependencies(), [nodeB]);
            assert.deepEqual(nodeB.getDependents(), [nodeA]);
        });
        it('throw when trying to add a dependency on itself', () => {
            const nodeA = makeFakeNode('1');
            assert.throws(() => nodeA.addDependency(nodeA));
        });
    });
    describe('.isDependentOn', () => {
        it('should identify the dependency relationships', () => {
            const graph = createComplexGraph();
            const nodes = Object.values(graph);
            const { nodeA, nodeB, nodeD, nodeF, nodeH } = graph;
            for (const node of nodes) {
                assert.strictEqual(nodeA.isDependentOn(node), node === nodeA);
                assert.strictEqual(nodeB.isDependentOn(node), node === nodeA || node === nodeB);
                assert.strictEqual(nodeH.isDependentOn(node), node !== nodeF);
            }
            assert.isTrue(nodeD.isDependentOn(nodeA));
            assert.isTrue(nodeD.isDependentOn(nodeB));
            assert.isTrue(nodeD.isDependentOn(nodeD));
            assert.isFalse(nodeD.isDependentOn(nodeH));
            assert.isTrue(nodeH.isDependentOn(nodeD));
            assert.isFalse(nodeF.isDependentOn(nodeH));
            assert.isFalse(nodeH.isDependentOn(nodeF));
        });
    });
    describe('.getRootNode', () => {
        it('should return the root node', () => {
            const graph = createComplexGraph();
            assert.strictEqual(graph.nodeA.getRootNode(), graph.nodeA);
            assert.strictEqual(graph.nodeB.getRootNode(), graph.nodeA);
            assert.strictEqual(graph.nodeD.getRootNode(), graph.nodeA);
            assert.strictEqual(graph.nodeF.getRootNode(), graph.nodeA);
        });
    });
    describe('.cloneWithoutRelationships', () => {
        it('should create a copy', () => {
            const node = makeFakeNode('1');
            const neighbor = makeFakeNode('2');
            node.addDependency(neighbor);
            const clone = node.cloneWithoutRelationships();
            assert.strictEqual(clone.id, '1');
            assert.notEqual(node, clone);
            assert.lengthOf(clone.getDependencies(), 0);
        });
        it('should copy isMainDocument', () => {
            const node = makeFakeNode('1');
            node.setIsMainDocument(true);
            const networkNode = new NetworkNode({});
            networkNode.setIsMainDocument(true);
            assert.isOk(node.cloneWithoutRelationships().isMainDocument());
            assert.isOk(networkNode.cloneWithoutRelationships().isMainDocument());
        });
    });
    describe('.cloneWithRelationships', () => {
        it('should create a copy of a basic graph', () => {
            const node = makeFakeNode('1');
            const neighbor = makeFakeNode('2');
            node.addDependency(neighbor);
            const clone = node.cloneWithRelationships();
            assert.strictEqual(clone.id, '1');
            assert.notEqual(node, clone);
            const dependencies = clone.getDependencies();
            assert.lengthOf(dependencies, 1);
            const neighborClone = dependencies[0];
            assert.strictEqual(neighborClone.id, neighbor.id);
            assert.notEqual(neighborClone, neighbor);
            assert.strictEqual(neighborClone.getDependents()[0], clone);
        });
        it('should create a copy of a complex graph', () => {
            const graph = createComplexGraph();
            const clone = graph.nodeA.cloneWithRelationships();
            const clonedIdMap = new Map();
            clone.traverse(node => clonedIdMap.set(node.id, node));
            assert.strictEqual(clonedIdMap.size, 8);
            graph.nodeA.traverse(node => {
                const clone = clonedIdMap.get(node.id);
                assert.strictEqual(clone.id, node.id);
                assert.notEqual(clone, node);
                const actualDependents = sortedById(clone.getDependents());
                const expectedDependents = sortedById(node.getDependents());
                actualDependents.forEach((cloneDependent, index) => {
                    const originalDependent = expectedDependents[index];
                    assert.strictEqual(cloneDependent.id, originalDependent.id);
                    assert.notEqual(cloneDependent, originalDependent);
                });
            });
        });
        it('should create a copy of a graph with long dependency chains', () => {
            //   C - D - E - F
            //  /             \
            // A - - - - - - - B
            const nodeA = makeFakeNode('A');
            const nodeB = makeFakeNode('B');
            const nodeC = makeFakeNode('C');
            const nodeD = makeFakeNode('D');
            const nodeE = makeFakeNode('E');
            const nodeF = makeFakeNode('F');
            nodeA.addDependent(nodeB);
            nodeF.addDependent(nodeB);
            nodeA.addDependent(nodeC);
            nodeC.addDependent(nodeD);
            nodeD.addDependent(nodeE);
            nodeE.addDependent(nodeF);
            const clone = nodeA.cloneWithRelationships();
            const clonedIdMap = new Map();
            clone.traverse(node => clonedIdMap.set(node.id, node));
            assert.strictEqual(clonedIdMap.size, 6);
        });
        it('should create a copy when not starting at root node', () => {
            const graph = createComplexGraph();
            const cloneD = graph.nodeD.cloneWithRelationships();
            assert.strictEqual(cloneD.id, 'D');
            assert.strictEqual(cloneD.getRootNode().id, 'A');
        });
        it('should create a partial copy of a complex graph', () => {
            const graph = createComplexGraph();
            // create a clone with F and all its dependencies
            const clone = graph.nodeA.cloneWithRelationships(node => node.id === 'F');
            const clonedIdMap = new Map();
            clone.traverse(node => clonedIdMap.set(node.id, node));
            assert.strictEqual(clonedIdMap.size, 6);
            assert.isOk(clonedIdMap.has('F'), 'did not include target node');
            assert.isOk(clonedIdMap.has('E'), 'did not include dependency');
            assert.isOk(clonedIdMap.has('B'), 'did not include branched dependency');
            assert.isOk(clonedIdMap.has('C'), 'did not include branched dependency');
            assert.isUndefined(clonedIdMap.get('G'));
            assert.isUndefined(clonedIdMap.get('H'));
        });
        it('should throw if original node is not in cloned graph', () => {
            const graph = createComplexGraph();
            assert.throws(
            // clone from root to nodeB, but called on nodeD
            () => graph.nodeD.cloneWithRelationships(node => node.id === 'B'), /^Cloned graph missing node$/);
        });
    });
    describe('.traverse', () => {
        it('should visit every dependent node', () => {
            const graph = createComplexGraph();
            const ids = [];
            graph.nodeA.traverse(node => ids.push(node.id));
            assert.deepEqual(ids, ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']);
        });
        it('should include a shortest traversal path to every dependent node', () => {
            const graph = createComplexGraph();
            const paths = [];
            graph.nodeA.traverse((node, traversalPath) => {
                assert.strictEqual(node.id, traversalPath[0].id);
                paths.push(traversalPath.map(node => node.id));
            });
            assert.deepEqual(paths, [
                ['A'],
                ['B', 'A'],
                ['C', 'A'],
                ['D', 'B', 'A'],
                ['E', 'D', 'B', 'A'],
                ['F', 'E', 'D', 'B', 'A'],
                ['G', 'E', 'D', 'B', 'A'],
                ['H', 'G', 'E', 'D', 'B', 'A'],
            ]);
        });
        it('should respect getNext', () => {
            const graph = createComplexGraph();
            const ids = [];
            graph.nodeF.traverse(node => ids.push(node.id), node => node.getDependencies());
            assert.deepEqual(ids, ['F', 'E', 'D', 'B', 'C', 'A']);
        });
    });
    describe('#hasCycle', () => {
        it('should return false for DAGs', () => {
            const graph = createComplexGraph();
            assert.isNull(BaseNode.findCycle(graph.nodeA));
        });
        it('should return false for triangular DAGs', () => {
            //   B
            //  / \
            // A - C
            const nodeA = makeFakeNode('A');
            const nodeB = makeFakeNode('B');
            const nodeC = makeFakeNode('C');
            nodeA.addDependent(nodeC);
            nodeA.addDependent(nodeB);
            nodeB.addDependent(nodeC);
            assert.isNull(BaseNode.findCycle(nodeA));
        });
        it('should return true for basic cycles', () => {
            // A - B - C - A!
            const nodeA = makeFakeNode('A');
            const nodeB = makeFakeNode('B');
            const nodeC = makeFakeNode('C');
            nodeA.addDependent(nodeB);
            nodeB.addDependent(nodeC);
            nodeC.addDependent(nodeA);
            assert.isNotNull(BaseNode.findCycle(nodeA));
        });
        it('should return true for children', () => {
            //       A!
            //      /
            // A - B - C
            const nodeA = makeFakeNode('A');
            const nodeB = makeFakeNode('B');
            const nodeC = makeFakeNode('C');
            nodeA.addDependent(nodeB);
            nodeB.addDependent(nodeC);
            nodeB.addDependent(nodeA);
            assert.isNotNull(BaseNode.findCycle(nodeC));
        });
        it('should return true for complex cycles', () => {
            //   B - D - F - G - C!
            //  /      /
            // A - - C - E - H
            const nodeA = makeFakeNode('A');
            const nodeB = makeFakeNode('B');
            const nodeC = makeFakeNode('C');
            const nodeD = makeFakeNode('D');
            const nodeE = makeFakeNode('E');
            const nodeF = makeFakeNode('F');
            const nodeG = makeFakeNode('G');
            const nodeH = makeFakeNode('H');
            nodeA.addDependent(nodeB);
            nodeA.addDependent(nodeC);
            nodeB.addDependent(nodeD);
            nodeC.addDependent(nodeE);
            nodeC.addDependent(nodeF);
            nodeD.addDependent(nodeF);
            nodeE.addDependent(nodeH);
            nodeF.addDependent(nodeG);
            nodeG.addDependent(nodeC);
            assert.isNotNull(BaseNode.findCycle(nodeA));
            assert.isNotNull(BaseNode.findCycle(nodeB));
            assert.isNotNull(BaseNode.findCycle(nodeC));
            assert.isNotNull(BaseNode.findCycle(nodeD));
            assert.isNotNull(BaseNode.findCycle(nodeE));
            assert.isNotNull(BaseNode.findCycle(nodeF));
            assert.isNotNull(BaseNode.findCycle(nodeG));
            assert.isNotNull(BaseNode.findCycle(nodeH));
        });
        it('works for very large graphs', () => {
            const root = makeFakeNode('root');
            let lastNode = root;
            for (let i = 0; i < 10000; i++) {
                const nextNode = makeFakeNode(`child${i}`);
                lastNode.addDependent(nextNode);
                lastNode = nextNode;
            }
            lastNode.addDependent(root);
            assert.isNotNull(BaseNode.findCycle(root));
        });
    });
});
//# sourceMappingURL=BaseNode.test.js.map