package com.loomcom.symon;

import com.loomcom.symon.devices.Memory;
import com.loomcom.symon.exceptions.MemoryAccessException;
import com.loomcom.symon.exceptions.MemoryRangeException;
import org.junit.*;

import static org.junit.Assert.*;

public class CpuImpliedModeTest {

    protected Cpu cpu;
    protected Bus bus;
    protected Memory mem;

    @Before
    public void setUp() throws MemoryRangeException, MemoryAccessException {
        this.cpu = new Cpu();
        this.bus = new Bus(0x0000, 0xffff);
        this.mem = new Memory(0x0000, 0xffff);
        bus.addCpu(cpu);
        bus.addDevice(mem);

        // Load the reset vector.
        bus.write(0xfffc, Bus.DEFAULT_LOAD_ADDRESS & 0x00ff);
        bus.write(0xfffd, (Bus.DEFAULT_LOAD_ADDRESS & 0xff00) >>> 8);

        cpu.reset();

        // Assert initial state
        assertEquals(0, cpu.getAccumulator());
        assertEquals(0, cpu.getXRegister());
        assertEquals(0, cpu.getYRegister());
        assertEquals(0x200, cpu.getProgramCounter());
        assertEquals(0xff, cpu.getStackPointer());
        assertEquals(0x24, cpu.getProcessorStatus());
    }

    /*
     * The following opcodes are tested for correctness in this file:
     *
     * BRK - $00
     * CLC - $18
     * CLD - $d8
     * CLI - $58
     * CLV - $B8
     *
     * DEX - $ca
     * DEY - $88
     * INX - $e8
     * INY - $c8
     * NOP - $ea
     *
     * PHA - $48
     * PHP - $08
     * PLA - $68
     * PLP - $28
     * RTI - $40
     *
     * RTS - $60
     * SEC - $38
     * SED - $f8
     * SEI - $78
     * TAX - $aa
     *
     * TAY - $a8
     * TSX - $ba
     * TXA - $8a
     * TXS - $9a
     * TYA - $98
     */

    /* BRK Tests - 0x00 */

    @Test
    public void test_BRK() throws MemoryAccessException {
        cpu.setCarryFlag();
        cpu.setOverflowFlag();
        cpu.clearIrqDisableFlag();
        assertEquals(0x20 | Cpu.P_CARRY | Cpu.P_OVERFLOW,
                     cpu.getProcessorStatus());
        assertEquals(0x00, cpu.stackPeek());
        assertFalse(cpu.getBreakFlag());
        assertEquals(0x0200, cpu.getProgramCounter());
        assertEquals(0xff, cpu.getStackPointer());

        // Set the IRQ vector
        bus.write(0xffff, 0x12);
        bus.write(0xfffe, 0x34);

        bus.loadProgram(0xea,  // NOP
                        0xea,  // NOP
                        0xea,  // NOP
                        0x00,  // BRK
                        0xea,  // NOP
                        0xea); // NOP

        cpu.step(3); // Three NOP instructions

        assertEquals(0x203, cpu.getProgramCounter());

        cpu.step(); // Triggers the BRK

        // Was at PC = 0x204.  PC+1 should now be on the stack
        assertEquals(0x02, bus.read(0x1ff, true)); // PC high byte
        assertEquals(0x05, bus.read(0x1fe, true)); // PC low byte
        assertEquals(0x20 | Cpu.P_CARRY | Cpu.P_OVERFLOW | Cpu.P_BREAK,
                     bus.read(0x1fd, true));       // Processor Status, with B set

        // Interrupt vector held 0x1234, so we should be there.
        assertEquals(0x1234, cpu.getProgramCounter());
        assertEquals(0xfc, cpu.getStackPointer());

        // B and I flags should have been set on P
        assertEquals(0x20 | Cpu.P_CARRY | Cpu.P_OVERFLOW | Cpu.P_BREAK |
                             Cpu.P_IRQ_DISABLE,
                     cpu.getProcessorStatus());
    }

    @Test
    public void test_BRK_IgnoresIrqDisableFlag() throws MemoryAccessException {
        cpu.setIrqDisableFlag();

        cpu.setCarryFlag();
        cpu.setOverflowFlag();
        assertEquals(0x20 | Cpu.P_CARRY | Cpu.P_OVERFLOW | Cpu.P_IRQ_DISABLE,
                     cpu.getProcessorStatus());
        assertEquals(0x00, cpu.stackPeek());
        assertFalse(cpu.getBreakFlag());
        assertEquals(0x0200, cpu.getProgramCounter());
        assertEquals(0xff, cpu.getStackPointer());
        
        // Set the IRQ vector
        bus.write(0xffff, 0x12);
        bus.write(0xfffe, 0x34);

        bus.loadProgram(0xea,
                        0xea,
                        0xea,
                        0x00,
                        0xea,
                        0xea);

        cpu.step(3); // Three NOP instructions

        assertEquals(0x203, cpu.getProgramCounter());

        // Triggers the BRK
        cpu.step();


        // Was at PC = 0x204.  PC+1 should now be on the stack
        assertEquals(0x02, bus.read(0x1ff, true)); // PC high byte
        assertEquals(0x05, bus.read(0x1fe, true)); // PC low byte
        assertEquals(0x20 | Cpu.P_CARRY | Cpu.P_OVERFLOW | Cpu.P_BREAK | Cpu.P_IRQ_DISABLE,
                     bus.read(0x1fd, true));       // Processor Status, with B set

        // Interrupt vector held 0x1234, so we should be there.
        assertEquals(0x1234, cpu.getProgramCounter());
        assertEquals(0xfc, cpu.getStackPointer());

        // B and I flags should have been set on P
        assertEquals(0x20 | Cpu.P_CARRY | Cpu.P_OVERFLOW | Cpu.P_BREAK | Cpu.P_IRQ_DISABLE,
                     cpu.getProcessorStatus());
    }


    /* CLC - Clear Carry Flag - $18 */
    @Test
    public void test_CLC() throws MemoryAccessException {
        cpu.setCarryFlag();
        assertTrue(cpu.getCarryFlag());

        bus.loadProgram(0x18);
        cpu.step();

        assertFalse(cpu.getCarryFlag());
    }

    /* CLD - Clear Decimal Mode Flag - $d8 */
    @Test
    public void test_CLD() throws MemoryAccessException {
        cpu.setDecimalModeFlag();
        assertTrue(cpu.getDecimalModeFlag());

        bus.loadProgram(0xd8);
        cpu.step();

        assertFalse(cpu.getDecimalModeFlag());
    }

    /* CLI - Clear Interrupt Disabled Flag - $58 */
    @Test
    public void test_CLI() throws MemoryAccessException {
        cpu.setIrqDisableFlag();
        assertTrue(cpu.getIrqDisableFlag());

        bus.loadProgram(0x58);
        cpu.step();

        assertFalse(cpu.getIrqDisableFlag());
    }

    /* CLV - Clear Overflow Flag - $b8 */
    @Test
    public void test_CLV() throws MemoryAccessException {
        cpu.setOverflowFlag();
        assertTrue(cpu.getOverflowFlag());

        bus.loadProgram(0xb8);
        cpu.step();

        assertFalse(cpu.getOverflowFlag());
    }

    /* DEX - Decrement the X register - $ca */
    @Test
    public void test_DEX() throws MemoryAccessException {
        bus.loadProgram(0xca);
        cpu.setXRegister(0x02);
        cpu.step();
        assertEquals(0x01, cpu.getXRegister());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_DEX_SetsZeroFlagWhenZero() throws MemoryAccessException {
        bus.loadProgram(0xca);
        cpu.setXRegister(0x01);
        cpu.step();
        assertEquals(0x00, cpu.getXRegister());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_DEX_SetsNegativeFlagWhen() throws MemoryAccessException {
        bus.loadProgram(0xca);
        cpu.step();
        assertEquals(0xff, cpu.getXRegister());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }

    /* DEY - Decrement the Y register - $88 */
    @Test
    public void test_DEY() throws MemoryAccessException {
        bus.loadProgram(0x88);
        cpu.setYRegister(0x02);
        cpu.step();
        assertEquals(0x01, cpu.getYRegister());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_DEY_SetsZeroFlagWhenZero() throws MemoryAccessException {
        bus.loadProgram(0x88);
        cpu.setYRegister(0x01);
        cpu.step();
        assertEquals(0x00, cpu.getYRegister());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_DEY_SetsNegativeFlagWhen() throws MemoryAccessException {
        bus.loadProgram(0x88);
        cpu.step();
        assertEquals(0xff, cpu.getYRegister());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }

    /* INX - Increment the X register - $e8 */
    @Test
    public void test_INX() throws MemoryAccessException {
        bus.loadProgram(0xe8);
        cpu.step();
        assertEquals(0x01, cpu.getXRegister());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_INX_SetsNegativeFlagWhenNegative() throws MemoryAccessException {
        bus.loadProgram(0xe8);
        cpu.setXRegister(0x7f);
        cpu.step();
        assertEquals(0x80, cpu.getXRegister());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }

    @Test
    public void test_INX_SetsZeroFlagWhenZero() throws MemoryAccessException {
        bus.loadProgram(0xe8);
        cpu.setXRegister(0xff);
        cpu.step();
        assertEquals(0x00, cpu.getXRegister());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    /* INY - Increment the Y register - $c8 */
    @Test
    public void test_INY() throws MemoryAccessException {
        bus.loadProgram(0xc8);
        cpu.step();
        assertEquals(0x01, cpu.getYRegister());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_INY_SetsNegativeFlagWhenNegative() throws MemoryAccessException {
        bus.loadProgram(0xc8);
        cpu.setYRegister(0x7f);
        cpu.step();
        assertEquals(0x80, cpu.getYRegister());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }

    @Test
    public void test_INY_SetsZeroFlagWhenZero() throws MemoryAccessException {
        bus.loadProgram(0xc8);
        cpu.setYRegister(0xff);
        cpu.step();
        assertEquals(0x00, cpu.getYRegister());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    /* NOP - No Operation - $ea */
    @Test
    public void test_NOP() throws MemoryAccessException {
        bus.loadProgram(0xea);
        cpu.step();
        // Should just not change anything except PC
        assertEquals(0, cpu.getAccumulator());
        assertEquals(0, cpu.getXRegister());
        assertEquals(0, cpu.getYRegister());
        assertEquals(0x201, cpu.getProgramCounter());
        assertEquals(0xff, cpu.getStackPointer());
        assertEquals(0x24, cpu.getProcessorStatus());
    }

    /* PHA - Push Accumulator - $48 */
    @Test
    public void test_PHA() throws MemoryAccessException {
        bus.loadProgram(0x48);
        cpu.setAccumulator(0x3a);
        cpu.step();
        assertEquals(0xfe, cpu.getStackPointer());
        assertEquals(0x3a, cpu.stackPeek());
    }

    /* PHP - Push Processor Status - $08 */
    @Test
    public void test_PHP() throws MemoryAccessException {
        bus.loadProgram(0x08);
        cpu.setProcessorStatus(0x27);
        cpu.step();
        assertEquals(0xfe, cpu.getStackPointer());
        // PHP should have set the BREAK flag _on the stack_ (but not in the CPU)
        assertEquals(0x37, cpu.stackPeek());
        assertEquals(0x27, cpu.getProcessorStatus());
    }

    /* PLA - Pul Accumulator - $68 */
    @Test
    public void test_PLA() throws MemoryAccessException {
        cpu.stackPush(0x32);
        bus.loadProgram(0x68);
        cpu.step();
        assertEquals(0x32, cpu.getAccumulator());
        assertFalse(cpu.getNegativeFlag());
        assertFalse(cpu.getZeroFlag());
    }

    @Test
    public void test_PLA_SetsZeroIfAccumulatorIsZero() throws MemoryAccessException {
        cpu.stackPush(0x00);
        bus.loadProgram(0x68);
        cpu.step();
        assertEquals(0x00, cpu.getAccumulator());
        assertFalse(cpu.getNegativeFlag());
        assertTrue(cpu.getZeroFlag());
    }

    @Test
    public void test_PLA_SetsNegativeIfAccumulatorIsNegative() throws MemoryAccessException {
        cpu.stackPush(0xff);
        bus.loadProgram(0x68);
        cpu.step();
        assertEquals(0xff, cpu.getAccumulator());
        assertTrue(cpu.getNegativeFlag());
        assertFalse(cpu.getZeroFlag());
    }

    /* PLP - Pull Processor Status - $28 */
    @Test
    public void test_PLP() throws MemoryAccessException {
        cpu.stackPush(0x2f);
        bus.loadProgram(0x28);
        cpu.step();
        assertEquals(0x2f, cpu.getProcessorStatus());
    }

    /* RTI - Return from Interrupt - $40 */
    @Test
    public void test_RTI() throws MemoryAccessException {
        cpu.stackPush(0x0f); // PC hi
        cpu.stackPush(0x11); // PC lo
        cpu.stackPush(0x29); // status

        bus.loadProgram(0x40);
        cpu.step();

        assertEquals(0x0f11, cpu.getProgramCounter());
        assertEquals(0x29, cpu.getProcessorStatus());
    }

    /* RTS - Return from Subroutine - $60 */
    @Test
    public void test_RTS() throws MemoryAccessException {
        cpu.stackPush(0x0f); // PC hi
        cpu.stackPush(0x11); // PC lo

        bus.loadProgram(0x60);
        cpu.step();

        assertEquals(0x0f12, cpu.getProgramCounter());
        assertEquals(0x24, cpu.getProcessorStatus());
    }

    /* SEC - Set Carry Flag - $38 */
    @Test
    public void test_SEC() throws MemoryAccessException {
        bus.loadProgram(0x38);
        cpu.step();
        assertTrue(cpu.getCarryFlag());
    }

    /* SED - Set Decimal Mode Flag - $f8 */
    @Test
    public void test_SED() throws MemoryAccessException {
        bus.loadProgram(0xf8);
        cpu.step();
        assertTrue(cpu.getDecimalModeFlag());
    }

    /* SEI - Set Interrupt Disable Flag - $78 */
    @Test
    public void test_SEI() throws MemoryAccessException {
        bus.loadProgram(0x78);
        cpu.step();
        assertTrue(cpu.getIrqDisableFlag());
    }

    /* TAX - Transfer Accumulator to X Register - $aa */
    @Test
    public void test_TAX() throws MemoryAccessException {
        cpu.setAccumulator(0x32);
        bus.loadProgram(0xaa);
        cpu.step();
        assertEquals(0x32, cpu.getXRegister());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TAX_SetsZeroFlagIfXIsZero() throws MemoryAccessException {
        cpu.setAccumulator(0x00);
        bus.loadProgram(0xaa);
        cpu.step();
        assertEquals(0x00, cpu.getXRegister());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TAX_SetsNegativeFlagIfXIsNegative() throws MemoryAccessException {
        cpu.setAccumulator(0xff);
        bus.loadProgram(0xaa);
        cpu.step();
        assertEquals(0xff, cpu.getXRegister());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }

    /* TAY - Transfer Accumulator to Y Register - $a8 */
    @Test
    public void test_TAY() throws MemoryAccessException {
        cpu.setAccumulator(0x32);
        bus.loadProgram(0xa8);
        cpu.step();
        assertEquals(0x32, cpu.getYRegister());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TAY_SetsZeroFlagIfYIsZero() throws MemoryAccessException {
        cpu.setAccumulator(0x00);
        bus.loadProgram(0xa8);
        cpu.step();
        assertEquals(0x00, cpu.getYRegister());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TAY_SetsNegativeFlagIfYIsNegative() throws MemoryAccessException {
        cpu.setAccumulator(0xff);
        bus.loadProgram(0xa8);
        cpu.step();
        assertEquals(0xff, cpu.getYRegister());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }

    /* TSX - Transfer Stack Pointer to X Register - $ba */
    @Test
    public void test_TSX() throws MemoryAccessException {
        cpu.setStackPointer(0x32);
        bus.loadProgram(0xba);
        cpu.step();
        assertEquals(0x32, cpu.getXRegister());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TSX_SetsZeroFlagIfXIsZero() throws MemoryAccessException {
        cpu.setStackPointer(0x00);
        bus.loadProgram(0xba);
        cpu.step();
        assertEquals(0x00, cpu.getXRegister());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TSX_SetsNegativeFlagIfXIsNegative() throws MemoryAccessException {
        cpu.setStackPointer(0xff);
        bus.loadProgram(0xba);
        cpu.step();
        assertEquals(0xff, cpu.getXRegister());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }

    /* TXA - Transfer X Register to Accumulator - $8a */
    @Test
    public void test_TXA() throws MemoryAccessException {
        cpu.setXRegister(0x32);
        bus.loadProgram(0x8a);
        cpu.step();
        assertEquals(0x32, cpu.getAccumulator());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TXA_SetsZeroFlagIfAccumulatorIsZero() throws MemoryAccessException {
        cpu.setXRegister(0x00);
        bus.loadProgram(0x8a);
        cpu.step();
        assertEquals(0x00, cpu.getAccumulator());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TXA_SetsNegativeFlagIfAccumulatorIsNegative() throws MemoryAccessException {
        cpu.setXRegister(0xff);
        bus.loadProgram(0x8a);
        cpu.step();
        assertEquals(0xff, cpu.getAccumulator());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }

    /* TXS - Transfer X Register to Stack Pointer - $9a */
    @Test
    public void test_TXS() throws MemoryAccessException {
        cpu.setXRegister(0x32);
        bus.loadProgram(0x9a);
        cpu.step();
        assertEquals(0x32, cpu.getStackPointer());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TXS_DoesNotAffectProcessorStatus() throws MemoryAccessException {
        cpu.setXRegister(0x00);
        bus.loadProgram(0x9a);
        cpu.step();
        assertEquals(0x00, cpu.getStackPointer());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());

        cpu.setXRegister(0x80);
        bus.loadProgram(0x9a);
        cpu.step();
        assertEquals(0x80, cpu.getStackPointer());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    /* TYA - Transfer Y Register to Accumulator - $98 */
    @Test
    public void test_TYA() throws MemoryAccessException {
        cpu.setYRegister(0x32);
        bus.loadProgram(0x98);
        cpu.step();
        assertEquals(0x32, cpu.getAccumulator());
        assertFalse(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TYA_SetsZeroFlagIfAccumulatorIsZero() throws MemoryAccessException {
        cpu.setYRegister(0x00);
        bus.loadProgram(0x98);
        cpu.step();
        assertEquals(0x00, cpu.getAccumulator());
        assertTrue(cpu.getZeroFlag());
        assertFalse(cpu.getNegativeFlag());
    }

    @Test
    public void test_TYA_SetsNegativeFlagIfAccumulatorIsNegative() throws MemoryAccessException {
        cpu.setYRegister(0xff);
        bus.loadProgram(0x98);
        cpu.step();
        assertEquals(0xff, cpu.getAccumulator());
        assertFalse(cpu.getZeroFlag());
        assertTrue(cpu.getNegativeFlag());
    }
}
