// Unit tests for AStar algorithm


class Pathfinder extends AStar {
        
    constructor(map = [], size = 0) {
        super(getManhattanDistance);
        this.domain = new Pathfinder.Domain(map, size);
    }

    static Node = class extends AStar.Node {
        constructor(key = [], g = 0, h = 0, info = '', parentKey = []) {
            super(key, g, h, info, parentKey);
        }
        
        // Updates the g, h and f costs and parent key of this node, then recomputes
        // the info that shows direction (N/S/E/W) from the new parent to this node.
        update(parentKey, g, h) {
            super.update(parentKey, g, h);
            // Determine the direction from the parent to this node.
            if (parentKey.length > 1) {
                let dy = this.key[0] - parentKey[0];
                let dx = this.key[1] - parentKey[1];
                if (dx == 1) this.info = 'E';
                else if (dx == -1) this.info = 'W';
                else if (dy == 1) this.info = 'S';
                else if (dy == -1) this.info = 'N';
            }
        }
    }

    static Domain = class extends AStar.Domain {
        constructor(map, size) {
            super();
            this.map = map;
            this.size = size;
        }

        newNode(key) {
            return new Pathfinder.Node(key);
        }

        getAdjacents(key) {
            let j = key[0];
            let i = key[1];
            let array = [];
            if (j >= 0 && j < this.size && i >= 0 && i < this.size) {
                if (i > 0 && this.map[j * this.size + i - 1] == 0)
                    array.push(new Pathfinder.Node([j, i - 1], 10));
                if (i < this.size - 1 && this.map[j * this.size + i + 1] == 0)
                    array.push(new Pathfinder.Node([j, i + 1], 10));
                if (j > 0 && this.map[(j - 1) * this.size + i] == 0)
                    array.push(new Pathfinder.Node([j - 1, i], 10));
                if (j < this.size - 1 && this.map[(j + 1) * this.size + i] == 0)
                    array.push(new Pathfinder.Node([j + 1, i], 10));
            }
            return array;
        }
    }
}


// The following code contains unit test function that test the functionality 
// of the classes and functions for A* search.

// Non evaluated selftest. It tests the basic functionality of the Node, 
// HashTable, and PriorityQueue
function test0() {
    {
        let closedlist = new HashTable();
        let actual = closedlist.find([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
        let expected = null;

        compareAndReport('Test 0_1', actual, expected, true);
    }

    {
        let closedlist = new HashTable();
        closedlist.add(new Pathfinder.Node([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10, 0, 'N'));
        let result1 = closedlist.find([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
        let result2 = closedlist.find([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); // Repeat
        let actual = result1.toString() + '|' + result2.toString();
        let expected = '0,1,2,3,4,5,6,7,8,9 10 0 10 N|0,1,2,3,4,5,6,7,8,9 10 0 10 N';

        compareAndReport('Test 0_2', actual, expected, true);
    }

    {
        let closedlist = new HashTable();
        closedlist.add(new Pathfinder.Node([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10, 0, 'N'));
        closedlist.add(new Pathfinder.Node([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10, 0, 'S')); // Reset
        let result = closedlist.find([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
        let actual = result.toString();
        let expected = '0,1,2,3,4,5,6,7,8,9 10 0 10 S';

        compareAndReport('Test 0_3', actual, expected, true);
    }

    {
        let closedlist = new HashTable();
        closedlist.add(new Pathfinder.Node([1, 1], 10, 0, 'S', [0, 1]));
        closedlist.add(new Pathfinder.Node([0, 1], 10, 0, 'E', [0, 0]));
        closedlist.add(new Pathfinder.Node([0, 0], 10, 0, '', []));
        let actual = closedlist.getPath([1, 1]).join(',');
        let expected = 'E,S';

        compareAndReport('Test 0_4', actual, expected, true);
    }

    {
        let q = new PriorityQueue();
        q.push(new Pathfinder.Node([0, 0], 3, 0, 'S'));
        q.push(new Pathfinder.Node([1, 1], 2, 0, 'N'));
        q.push(new Pathfinder.Node([2, 2], 5, 0, 'E'));
        let actual = q.toString();
        let expected = '|1,1 2 0 2 N||0,0 3 0 3 S||2,2 5 0 5 E|';

        compareAndReport('Test 0_5', actual, expected, true);
    }

    {
        let q = new PriorityQueue();
        q.push(new Pathfinder.Node([0, 0], 3, 0, 'S'));
        q.push(new Pathfinder.Node([1, 1], 2, 0, 'N'));
        q.push(new Pathfinder.Node([2, 2], 1, 0, 'E'));
        let actual = q.toString();
        let expected = '|2,2 1 0 1 E||1,1 2 0 2 N||0,0 3 0 3 S|';

        compareAndReport('Test 0_6', actual, expected, true);
    }

    {
        let q = new PriorityQueue();
        q.push(new Pathfinder.Node([0, 0], 3, 0, 'S'));
        q.push(new Pathfinder.Node([1, 1], 2, 0, 'N'));
        q.push(new Pathfinder.Node([2, 2], 1, 0, 'E'));
        let found = q.pop([1, 1]);
        let actual = found.toString() + q.toString();
        let expected = '1,1 2 0 2 N|2,2 1 0 1 E||0,0 3 0 3 S|';

        compareAndReport('Test 0_7', actual, expected, true);
    }

    {
        let q = new PriorityQueue();
        q.push(new Pathfinder.Node([0, 0], 30, 0, 'S'));
        q.push(new Pathfinder.Node([1, 1], 20, 0, 'N'));
        q.push(new Pathfinder.Node([2, 2], 10, 0, 'E'));
        q.update([1, 1], [1, 0], 1, 2);
        let actual = q.toString();
        let expected = '|1,1 1 2 3 E 1,0||2,2 10 0 10 E||0,0 30 0 30 S|';

        compareAndReport('Test 0_8', actual, expected, true);
    }
}

// Test 1 tests the GetMapAdjacents class. It checks if the run() method returns 
// all four adjacent nodes correctly.
function test1() {
    let map = [
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let adjacents = pathfinder.domain.getAdjacents([1, 1]);
    let actual = adjacents.map(node => node.toString()).join('|');
    let expected = '1,0 10 0 10|1,2 10 0 10|0,1 10 0 10|2,1 10 0 10';

    compareAndReport('Test 1', actual, expected);
}

// Test 2 tests the GetMapAdjacents class. It checks if the run() method returns
// adjacent nodes correctly excluding non-existing nodes.
function test2() {
    let map = [
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let adjacents = pathfinder.domain.getAdjacents([4, 4]);
    let actual = adjacents.map(node => node.toString()).join('|');
    let expected = '4,3 10 0 10|3,4 10 0 10';

    compareAndReport('Test 2', actual, expected);
}

// Test 3 tests the GetMapAdjacents class with a map that has obstacles. It
// checks if the adjacent nodes returned for specific coordinates correctly
// exclude the obstacles.
function test3() {
    let map = [
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let adjacents = pathfinder.domain.getAdjacents([3, 2]);
    let actual = adjacents.map(node => node.toString()).join('|');
    let expected = '3,3 10 0 10|4,2 10 0 10';

    compareAndReport('Test 3', actual, expected);
}

// Test 4 tests the AStar class with an empty map. It checks if the 
// pathfinding algorithm correctly handles the case where there are no valid
// paths.
async function test4() {
    let pathfinder = new Pathfinder();
    let path = await pathfinder.run([1, 1], [1, 1]);
    let actual = path.join(',');
    let expected = '';

    compareAndReport('Test 4', actual, expected);
}

// Test 5 tests the AStar class with a map and obstacles. It checks if 
// the pathfinding algorithm correctly identifies the start point as the same 
// as the end point and returns an empty path, indicating that no movement is 
// needed to reach the destination.
async function test5() {
    let map = [
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let path = await pathfinder.run([1, 1], [1, 1]);
    let actual = path.join(',');
    let expected = '';

    compareAndReport('Test 5', actual, expected);
}

// Test 6 tests the AStar class with a map that has no obstacles blocking 
// the path from the start point to the end point. It checks if the pathfinding
// algorithm can find the direct path from the left side to the 
// right side of the map without any detours.
async function test6() {
    let map = [
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let path = await pathfinder.run([1, 1], [1, 3]);
    let actual = path.join(',');
    let expected = 'E,E';

    compareAndReport('Test 6', actual, expected);
}

// Test 7 tests the AStar class with a map that has no obstacles blocking 
// the path from the start point to the end point. It checks if the pathfinding
// algorithm can find the direct path from the top-right corner to the 
// bottom-right corner without any detours.
async function test7() {
    let map = [
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let path = await pathfinder.run([0, 4], [4, 4]);
    let actual = path.join(',');
    let expected = 'S,S,S,S';

    compareAndReport('Test 7', actual, expected);
}

// Test 8 tests the AStar class with a map that has an obstacle blocking the
// direct path from the start point to the end point. It checks if the
// pathfinding algorithm can find an alternative path around the obstacle to 
// reach the destination.
async function test8() {
    let map = [
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let path = await pathfinder.run([1, 1], [3, 3]);
    let actual = path.join(',');
    let expected = 'E,E,S,S';

    compareAndReport('Test 8', actual, expected);
}

// Test 9 tests the AStar class with a map. End point is outside the map 
// boundaries. It checks if the pathfinding algorithm correctly identifies 
// that there is no valid path and returns an empty path.
async function test9() {
    let map = [
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let path = await pathfinder.run([1, 1], [30, 30]);
    let actual = path.join(',');
    let expected = [].join(',');

    compareAndReport('Test 9', actual, expected);
}

// Test 10 tests the AStar class with a more complex map that has multiple
// obstacles. It checks if the pathfinding algorithm can find the correct path
// from the top-left corner to the bottom-right corner while navigating around
// the obstacles.
async function test10() {
    let map = [
        0, 1, 0, 0, 0,
        0, 1, 0, 1, 0,
        0, 1, 0, 1, 0,
        0, 1, 0, 1, 0,
        0, 0, 0, 1, 0
    ];
    let pathfinder = new Pathfinder(map, 5);
    let path = await pathfinder.run([0, 0], [4, 4]);
    let actual = path.join(',');
    let expected = 'S,S,S,S,E,E,N,N,N,N,E,E,S,S,S,S';

    compareAndReport('Test 10', actual, expected);
}