//
//  krom65816Tests.swift
//  Clock Signal
//
//  Created by Thomas Harte on 02/11/2020.
//  Copyright 2020 Thomas Harte. All rights reserved.
//

import Foundation
import XCTest

// This utilises krom's SNES-centric 65816 tests, comparing step-by-step to
// the traces offered by LilaQ at emudev.de as I don't want to implement a
// SNES just for the sake of result inspection.
//
// So:
// https://github.com/PeterLemon/SNES/tree/master/CPUTest/CPU for the tests;
// https://emudev.de/q00-snes/65816-the-cpu/ for the traces.
class Krom65816Tests: XCTestCase {

	// MARK: - Test Runner

	func runTest(_ name: String, pcLimit: UInt32? = nil) {
		var testData: Data?
		if let filename = Bundle(for: type(of: self)).url(forResource: name, withExtension: "sfc") {
			testData = try? Data(contentsOf: filename)
		}

		var testOutput: String?
		if let filename = Bundle(for: type(of: self)).url(forResource: name + "-trace_compare", withExtension: "log") {
			testOutput = try? String(contentsOf: filename)
		}

		XCTAssertNotNil(testData)
		XCTAssertNotNil(testOutput)

		let outputLines = testOutput!.components(separatedBy: "\r\n")

		// Assumptions about the SFC file format follow; I couldn't find a spec but those
		// produced by krom appear just to be binary dumps. Fingers crossed!
		let machine = CSTestMachine6502(processor: .processor65816)
		machine.setData(testData!, atAddress: 0x8000)

		// This reproduces the state seen at the first line of all of LilaQ's traces;
		// TODO: determine whether (i) this is the SNES state at reset, or merely how
		// some sort of BIOS leaves it; and (ii) if the former, whether I have post-reset
		// state incorrect. I strongly suspect it's a SNES-specific artefact?
		machine.setValue(0x8000, for: .programCounter)
		machine.setValue(0x0000, for: .A)
		machine.setValue(0x0000, for: .X)
		machine.setValue(0x0000, for: .Y)
		machine.setValue(0x00ff, for: .stackPointer)
		machine.setValue(0x34, for: .flags)

		// There seems to be some Nintendo-special register at address 0x0000.
		machine.setValue(0xb5, forAddress: 0x0000)

		// Poke some fixed values for SNES registers to get past initial setup.
		machine.setValue(0x42, forAddress: 0x4210)	// "RDNMI", apparently; this says: CPU version 2, vblank interrupt request.
		var allowNegativeError = false

		var lineNumber = 1
		var previousPC = 0
		for line in outputLines {
			// At least one of the traces ends with an empty line; my preference is not to
			// modify the originals if possible.
			if line == "" {
				break
			}

			machine.runForNumber(ofInstructions: 1)
			let newPC = Int(machine.value(for: .lastOperationAddress))

			// Stop right now if a PC limit has been specified and this is it.
			if let pcLimit = pcLimit, pcLimit == newPC {
				break
			}

			func machineState() -> String {
				// Formulate my 65816 state in the same form as the test machine
				var cpuState = ""
				let emulationFlag = machine.value(for: .emulationFlag) != 0
				cpuState += String(format: "%06x ", machine.value(for: .lastOperationAddress))
				cpuState += String(format: "A:%04x ", machine.value(for: .A))
				cpuState += String(format: "X:%04x ", machine.value(for: .X))
				cpuState += String(format: "Y:%04x ", machine.value(for: .Y))
				if emulationFlag {
					cpuState += String(format: "S:01%02x ", machine.value(for: .stackPointer))
				} else {
					cpuState += String(format: "S:%04x ", machine.value(for: .stackPointer))
				}
				cpuState += String(format: "D:%04x ", machine.value(for: .direct))
				cpuState += String(format: "DB:%02x ", machine.value(for: .dataBank))

				let flags = machine.value(for: .flags)
				cpuState += (flags & 0x80) != 0 ? "N" : "n"
				cpuState += (flags & 0x40) != 0 ? "V" : "v"
				if emulationFlag {
					cpuState += "1B"
				} else {
					cpuState += (flags & 0x20) != 0 ? "M" : "m"
					cpuState += (flags & 0x10) != 0 ? "X" : "x"
				}
				cpuState += (flags & 0x08) != 0 ? "D" : "d"
				cpuState += (flags & 0x04) != 0 ? "I" : "i"
				cpuState += (flags & 0x02) != 0 ? "Z" : "z"
				cpuState += (flags & 0x01) != 0 ? "C" : "c"

				cpuState += " "

				return cpuState
			}

			// Permit a fix-up of the negative flag only if this line followed a test of $4210.
			var cpuState = machineState()
			if cpuState != line && allowNegativeError {
				machine.setValue(machine.value(for: .flags) ^ 0x80, for: .flags)
				cpuState = machineState()
			}

			XCTAssertEqual(cpuState, line, "Mismatch on line #\(lineNumber); after instruction #\(String(format:"%02x", machine.value(forAddress: UInt32(previousPC))))")
			if cpuState != line {
				break
			}
			lineNumber += 1
			previousPC = newPC

			// Check whether a 'RDNMI' toggle needs to happen by peeking at the next instruction;
			// if it's BIT $4210 then toggle the top bit at address $4210.
			//
			// Coupling here: assume that by the time the test 65816 is aware it's on a new instruction
			// it's because the actual 65816 has read a new opcode, and that if the 65816 has just read
			// a new opcode then it has already advanced the program counter.
			let programCounter = machine.value(for: .programCounter)
			let nextInstr = [
				machine.value(forAddress: UInt32(programCounter - 1)),
				machine.value(forAddress: UInt32(programCounter + 0)),
				machine.value(forAddress: UInt32(programCounter + 1))
			]
			allowNegativeError = nextInstr[0] == 0x2c && nextInstr[1] == 0x10 && nextInstr[2] == 0x42
		}
	}

	// MARK: - Tests

	func testADC() {	runTest("CPUADC")	}
	func testAND() {	runTest("CPUAND")	}
	func testASL() {	runTest("CPUASL")	}
	func testBIT() {	runTest("CPUBIT")	}
	func testBRA() {	runTest("CPUBRA")	}
	func testCMP() {	runTest("CPUCMP")	}
	func testDEC() {	runTest("CPUDEC")	}
	func testEOR() {	runTest("CPUEOR")	}
	func testINC() {	runTest("CPUINC")	}
	func testJMP() {	runTest("CPUJMP")	}
	func testLDR() {	runTest("CPULDR")	}
	func testLSR() {	runTest("CPULSR")	}
	func testMOV() {	runTest("CPUMOV")	}
	func testORA() {	runTest("CPUORA")	}
	func testPHL() {	runTest("CPUPHL")	}
	func testPSR() {	runTest("CPUPSR")	}
	func testROL() {	runTest("CPUROL")	}
	func testROR() {	runTest("CPUROR")	}
	func testSBC() {	runTest("CPUSBC")	}
	func testSTR() {	runTest("CPUSTR")	}
	func testTRN() {	runTest("CPUTRN")	}

	// Ensure the MSC tests stop before they attempt to test STP and WAI;
	// the test relies on SNES means for scheduling a future interrupt.
	func testMSC() {	runTest("CPUMSC", pcLimit: 0x8523)	}
}
