/**
 * Tom Harte test data based test suite
 *
 * Uses test files from https://github.com/TomHarte/ProcessorTests
 * To use, set TOM_HARTE_TEST_PATH to local copy of that repository
 */

import fs from 'fs';

import CPU6502, { FLAVOR_ROCKWELL_65C02, FLAVOR_WDC_65C02 } from 'js/cpu6502';
import { toHex } from 'js/util';
import type { byte, word } from 'js/types';

import { toReadableState } from './util/cpu';
import { TestMemory } from './util/memory';

// JEST_DETAIL=true converts decimal values to hex before testing
// expectations, for better readability at the expense of speed.
const detail = !!process.env.JEST_DETAIL;

/**
 * Types for JSON tests
 */

/**
 * Memory address and value
 */
type MemoryValue = [address: word, value: byte];

/**
 * Represents initial and final CPU and memory states
 */
 interface TestState {
    /** Program counter */
    pc: word;
    /** Stack register */
    s: byte;
    /** Accumulator */
    a: byte;
    /** X index */
    x: byte;
    /** Y index */
    y: byte;
    /** Processor status register */
    p: byte;
    /** M */
    ram: MemoryValue[];
}

/**
 * CPU cycle memory operation
 */
type Cycle = [address: word, value: byte, type: 'read'|'write'];

/**
 * One test record
 */
interface Test {
    /** Test name */
    name: string;
    /** Initial CPU register and memory state */
    initial: TestState;
    /**  Final CPU register and memory state */
    final: TestState;
    /** Detailed CPU cycles */
    cycles: Cycle[];
}

/**
 * Initialize cpu and memory before test
 *
 * @param cpu Target cpu instance
 * @param state Initial test state
 */
function initState(cpu: CPU6502, state: TestState) {
    const { pc, s, a, x, y, p, ram } = state;
    cpu.setState({ cycles: 0, pc, sp: s, a, x, y, s: p });

    for (let idx = 0; idx < ram.length; idx++) {
        const [address, mem] = ram[idx];
        cpu.write(address, mem);
    }
}

/**
 * Pretty print 'address: val' if detail is turned on,
 * or passes through raw test data if not.
 *
 * @returns string or raw test data
 */
function toAddrValHex([address, val]: MemoryValue) {
    if (detail) {
        return `${toHex(address, 4)}: ${toHex(val)}`;
    } else {
        return [address, val];
    }
}

/**
 * Pretty print 'address: val (read|write)' if detail is turned on,
 * or passes through raw test data if not.
 *
 * @returns string or raw test data
 */
function toAddrValHexType([address, val, type]: Cycle) {
    if (detail) {
        return `${toHex(address, 4)}: ${toHex(val)} ${type}`;
    } else {
        return [address, val, type];
    }
}

/**
 * Compare end state and read write behavior of test run
 *
 * @param cpu Test CPU
 * @param memory Test memory
 * @param test Test data to compare against
 */
function expectState(cpu: CPU6502, memory: TestMemory, test: Test) {
    const { pc, s, a, x, y, p, ram } = test.final;
    expect(
        toReadableState(cpu.getState())
    ).toEqual(
        toReadableState({cycles: test.cycles.length, pc, sp: s, a, x, y, s: p })
    );

    // Retrieve relevant memory locations and values
    const result = [];
    for (let idx = 0; idx < ram.length; idx++) {
        const [address] = ram[idx];
        result.push([address, cpu.read(address)]);
    }
    expect(
        result.map(toAddrValHex)
    ).toEqual(
        ram.map(toAddrValHex)
    );

    expect(
        memory.getLog().map(toAddrValHexType)
    ).toEqual(
        test.cycles.map(toAddrValHexType)
    );
}

interface OpTest {
    op: string;
    name: string;
    mode: string;
}

const testPath = process.env.TOM_HARTE_TEST_PATH;

// There are 10,0000 tests per test file, which would take several hours
// in jest. 16 is a manageable quantity that still gets good coverage.
const maxTests = 16;

if (testPath) {
    const testPath6502 = `${testPath}/6502/v1/`;
    const testPathWDC65C02 = `${testPath}/wdc65c02/v1/`;
    const testPathRW65C02 = `${testPath}/rockwell65c02/v1/`;

    const opAry6502: OpTest[] = [];
    const opAryRW65C02: OpTest[] = [];
    const opAryWDC65C02: OpTest[] = [];

    const buildOpArrays = () => {
        const cpu = new CPU6502();

        for (const op in cpu.OPS_6502) {
            const { name, mode } = cpu.OPS_6502[op];
            const test = { op: toHex(+op), name, mode };
            opAry6502.push(test);
            opAryRW65C02.push(test);
            opAryWDC65C02.push(test);
        }

        for (const op in cpu.OPS_NMOS_6502) {
            const { name, mode } = cpu.OPS_NMOS_6502[op];
            const test = { op: toHex(+op), name, mode };
            opAry6502.push(test);
        }

        for (const op in cpu.OPS_65C02) {
            const { name, mode } = cpu.OPS_65C02[op];
            const test = { op: toHex(+op), name, mode };
            opAryRW65C02.push(test);
            opAryWDC65C02.push(test);
        }

        // WDC 65C02 NOPs
        [
            '03', '0b', '13', '1b', '23', '2b', '33', '3b',
            '43', '4b', '53', '5b', '63', '6b', '73', '7b',
            '83', '8b', '93', '9b', 'a3', 'ab', 'b3', 'bb',
            'c3',       'd3',       'e3', 'eb', 'f3', 'fb'
        ].forEach((op) =>
            opAryWDC65C02.push({ op, name: 'nop', mode: 'implied'})
        );

        // Rockwell 65C02 NOPs
        [
            '03', '0b', '13', '1b', '23', '2b', '33', '3b',
            '43', '4b', '53', '5b', '63', '6b', '73', '7b',
            '83', '8b', '93', '9b', 'a3', 'ab', 'b3', 'bb',
            'c3', 'cb', 'd3', 'db', 'e3', 'eb', 'f3', 'fb'
        ].forEach((op) =>
            opAryRW65C02.push({ op, name: 'nop', mode: 'implied'})
        );
    };

    buildOpArrays();

    describe('Tom Harte', function() {
        let cpu: CPU6502;
        let memory: TestMemory;

        describe('NMOS 6502', function() {
            beforeAll(function() {
                cpu = new CPU6502();
                memory = new TestMemory(256);
                cpu.addPageHandler(memory);
            });

            describe.each(opAry6502)('Test op $op $name $mode', ({op}) => {
                const data = fs.readFileSync(`${testPath6502}${op}.json`, 'utf-8');
                const tests = JSON.parse(data) as Test[];

                it.each(tests.slice(0, maxTests))('Test $name', (test) => {
                    initState(cpu, test.initial);
                    memory.logStart();
                    cpu.step();
                    memory.logStop();
                    expectState(cpu, memory, test);
                });
            });
        });

        describe('Rockwell 65C02', function() {
            beforeAll(function() {
                cpu = new CPU6502({ flavor: FLAVOR_ROCKWELL_65C02 });
                memory = new TestMemory(256);
                cpu.addPageHandler(memory);
            });

            describe.each(opAryRW65C02)('Test op $op $name $mode', ({op}) => {
                const data = fs.readFileSync(`${testPathRW65C02}${op}.json`, 'utf-8');
                const tests = JSON.parse(data) as Test[];

                it.each(tests.slice(0, maxTests))('Test $name', (test) => {
                    initState(cpu, test.initial);
                    memory.logStart();
                    cpu.step();
                    memory.logStop();
                    expectState(cpu, memory, test);
                });
            });
        });

        describe('WDC 65C02', function() {
            beforeAll(function() {
                cpu = new CPU6502({ flavor: FLAVOR_WDC_65C02 });
                memory = new TestMemory(256);
                cpu.addPageHandler(memory);
            });

            describe.each(opAryWDC65C02)('Test op $op $name $mode', ({op}) => {
                const data = fs.readFileSync(`${testPathWDC65C02}${op}.json`, 'utf-8');
                const tests = JSON.parse(data) as Test[];

                it.each(tests.slice(0, maxTests))('Test $name', (test) => {
                    initState(cpu, test.initial);
                    memory.logStart();
                    cpu.step();
                    memory.logStop();
                    expectState(cpu, memory, test);
                });
            });
        });
    });
} else {
    test('Skipping Tom Harte tests', () => { expect(testPath).toBeFalsy(); });
}
