require 'evoasm/test'

require 'x64_helper'

module Evoasm
  module X64
    class EncodingTest < Minitest::Test
      include X64Helper

      def test_direct
        assert_disassembles_to 'add rax, rbx', :add_r64_rm64,
                               reg0: :a, reg1: :b
        assert_disassembles_to 'add rax, r12', :add_r64_rm64,
                               reg0: :a, reg1: :r12
        assert_disassembles_to 'add r11, rbx', :add_r64_rm64,
                               reg0: :r11, reg1: :b
        assert_disassembles_to 'add r11, r12', :add_r64_rm64,
                               reg0: :r11, reg1: :r12

        assert_disassembles_to 'add eax, ebx', :add_r32_rm32,
                               reg0: :a, reg1: :b
        assert_disassembles_to 'add eax, r12d', :add_r32_rm32,
                               reg0: :a, reg1: :r12
        assert_disassembles_to 'add r11d, ebx', :add_r32_rm32,
                               reg0: :r11, reg1: :b
        assert_disassembles_to 'add r11d, r12d', :add_r32_rm32,
                               reg0: :r11, reg1: :r12

        assert_disassembles_to 'add ax, bx', :add_r16_rm16,
                               reg0: :a, reg1: :b
        assert_disassembles_to 'add ax, r12w', :add_r16_rm16,
                               reg0: :a, reg1: :r12
        assert_disassembles_to 'add r11w, bx', :add_r16_rm16,
                               reg0: :r11, reg1: :b
        assert_disassembles_to 'add r11w, r12w', :add_r16_rm16,
                               reg0: :r11, reg1: :r12

        assert_disassembles_to 'movsx ax, dl', :movsx_r16_rm8,
                               reg0: :a, reg1: :d

        assert_raises Evoasm::Error do
          assemble :add_r32_rm32, reg0: :xmm11, reg1: :xmm12
        end

      end

      def test_indirect
        assert_disassembles_to 'add rax, qword ptr [rbx]', :add_r64_rm64,
                               reg0: :a, reg_base: :b
        assert_disassembles_to 'add r11, qword ptr [r12]', :add_r64_rm64,
                               reg0: :r11, reg_base: :r12
        assert_disassembles_to 'add eax, dword ptr [rbx]', :add_r32_rm32,
                               reg0: :a, reg_base: :b
        assert_disassembles_to 'add ax, word ptr [rbx]', :add_r16_rm16,
                               reg0: :a, reg_base: :b

        # reg1 should be ignored
        assert_disassembles_to 'add eax, dword ptr [rbx]', :add_r32_rm32,
                               reg0: :a, reg1: :c, reg_base: :b

        assert_disassembles_to 'add rax, qword ptr [rbx + rcx*4]', :add_r64_rm64,
                               reg0: :a, reg_base: :b, reg_index: :c, scale: 4
        assert_disassembles_to 'add rax, qword ptr [rbx + rcx]', :add_r64_rm64,
                               reg0: :a, reg_base: :b, reg_index: :c, scale: 1
        assert_disassembles_to 'add rax, qword ptr [rbx + rcx*8]', :add_r64_rm64,
                               reg0: :a, reg_base: :b, reg_index: :c, scale: 8
        assert_disassembles_to 'add r10, qword ptr [r11 + r12*4]', :add_r64_rm64,
                               reg0: :r10, reg_base: :r11, reg_index: :r12, scale: 4

        assert_raises Evoasm::Error do
          assemble :add_r64_rm64, reg0: :r10, reg_base: :xmm11
        end

        assert_raises Evoasm::Error do
          assemble :add_r64_rm64, reg0: :r10, reg_index: :xmm11
        end

        # Force SIB
        with_forced_sib = assemble :add_r64_rm64,
                                   reg0: :r10, reg_base: :r11,
                                   force_sib?: true

        without_forced_sib = assemble :add_r64_rm64,
                                      reg0: :r10, reg_base: :r11,
                                      force_sib?: false

        refute_equal with_forced_sib, without_forced_sib

        assert_equal 'add r10, qword ptr [r11]', disassemble(with_forced_sib)
        assert_equal 'add r10, qword ptr [r11]', disassemble(without_forced_sib)

        # SP/R12 always need SIB
        {sp: 'rsp', r12: 'r12'}.each do |reg_base, reg_base_name|

          with_forced_sib = assemble(:add_r64_rm64,
                                     reg0: :r10,
                                     reg_base: reg_base,
                                     force_sib?: true)

          without_forced_sib = assemble(:add_r64_rm64,
                                        reg0: :r10,
                                        reg_base: reg_base,
                                        force_sib?: false)

          assert_equal with_forced_sib, without_forced_sib

          assert_equal "add r10, qword ptr [#{reg_base_name}]",
                       disassemble(with_forced_sib)

          assert_equal "add r10, qword ptr [#{reg_base_name}]",
                       disassemble(without_forced_sib)
        end

        # Force 32-bit displacement
        with_disp8 = assemble :add_r64_rm64,
                              reg0: :r10, reg_base: :r11, disp: 0x10

        with_disp32 = assemble :add_r64_rm64,
                               reg0: :r10, reg_base: :r11, disp: 0x10, force_disp32?: true

        assert_equal 'add r10, qword ptr [r11 + 0x10]', disassemble(with_disp8)
        assert_equal 'add r10, qword ptr [r11 + 0x10]', disassemble(with_disp32)

        refute_equal with_disp8, with_disp32
      end

      def test_address_size
        assert_disassembles_to 'add rax, dword ptr [ebx]', :add_r64_rm64,
                               reg0: :a, reg_base: :b, addr_size: 32

        assert_disassembles_to 'add eax, dword ptr [ebx]', :add_r32_rm32,
                               reg0: :a, reg_base: :b, addr_size: 32
      end

      def test_lock
        assert_disassembles_to 'lock add qword ptr [rax], rbx', :add_rm64_r64,
                               reg_base: :a, reg1: :b, lock?: true

        assert_disassembles_to 'lock inc qword ptr [r10]', :inc_rm64,
                               reg_base: :r10, lock?: true
      end

      def test_rex
        assert_disassembles_to 'add rax, rbx', :add_r64_rm64,
                               reg0: :a, reg1: :b, force_rex?: true

        # needs REX (.W), even if not forced
        assert_assembles_to "\x48\x03\xC3", :add_r64_rm64,
                            reg0: :a, reg1: :b, force_rex?: false

        assert_assembles_to "\x48\x03\xC3", :add_r64_rm64,
                            reg0: :a, reg1: :b, force_rex?: true

        # REX.X is free
        assert_assembles_to "\x4A\x03\xC3", :add_r64_rm64,
                            reg0: :a, reg1: :b, rex_x: 0b1

        assert_disassembles_to 'add rax, rbx', :add_r64_rm64,
                               reg0: :a, reg1: :b, rex_x: 0b1

        # REX is optional
        assert_disassembles_to 'add eax, ebx', :add_r32_rm32,
                               reg0: :a, reg1: :b

        assert_assembles_to "\x03\xC3", :add_r32_rm32,
                            reg0: :a, reg1: :b

        assert_assembles_to "\x40\x03\xC3", :add_r32_rm32,
                            reg0: :a, reg1: :b, force_rex?: true

        assert_disassembles_to 'add eax, ebx', :add_r32_rm32,
                               reg0: :a, reg1: :b, force_rex?: true

        # REX.X is free
        assert_assembles_to "\x42\x03\xC3", :add_r32_rm32,
                            reg0: :a, reg1: :b, force_rex?: true, rex_x: 0b1

        # REX.X is free
        assert_assembles_to "\x40\x81\xC0\x10\x00\x00\x00", :add_rm32_imm32,
                            reg0: :a, imm0: 0x10, force_rex?: true

        assert_assembles_to "\x42\x81\xC0\x10\x00\x00\x00", :add_rm32_imm32,
                            reg0: :a, imm0: 0x10, force_rex?: true, rex_x: 0b1

        # REX.R is free (register is encoded in ModRM.rm)
        assert_assembles_to "\x44\x81\xC0\x10\x00\x00\x00", :add_rm32_imm32,
                            reg0: :a, imm0: 0x10, force_rex?: true, rex_r: 0b1

        assert_assembles_to "\x46\x81\xC0\x10\x00\x00\x00", :add_rm32_imm32,
                            reg0: :a, imm0: 0x10, force_rex?: true, rex_r: 0b1, rex_x: 0b1


      end

      def test_byte_reg

        assert_assembles_to "\x40\x02\xC3", :add_r8_rm8,
                            reg0: :a, reg1: :b, force_rex?: true

        assert_assembles_to "\x02\xC3", :add_r8_rm8,
                            reg0: :a, reg1: :b, force_rex?: false

        assert_assembles_to "\x40\x00\xD8", :add_rm8_r8,
                            reg0: :a, reg1: :b, force_rex?: true

        assert_assembles_to "\x00\xD8", :add_rm8_r8,
                            reg0: :a, reg1: :b, force_rex?: false

        %i(add_r8_rm8 add_rm8_r8).each do |inst|
          assert_disassembles_to 'add al, bl', inst,
                                 reg0: :a, reg1: :b

          assert_disassembles_to 'add r11b, r12b', inst,
                                 reg0: :r11, reg1: :r12

          assert_disassembles_to 'add al, r12b', inst,
                                 reg0: :a, reg1: :r12

          assert_disassembles_to 'add al, bl', inst,
                                 reg0: :a, reg1: :b,
                                 force_rex?: true

          assert_disassembles_to 'add ah, bl', inst,
                                 reg0: :a, reg1: :b,
                                 reg0_high_byte?: true

          assert_disassembles_to 'add ah, al', inst,
                                 reg0: :a, reg1: :a,
                                 reg0_high_byte?: true

          assert_disassembles_to 'add ah, bh', inst,
                                 reg0: :a, reg1: :b,
                                 reg0_high_byte?: true,
                                 reg1_high_byte?: true

          assert_disassembles_to 'add al, bh', inst,
                                 reg0: :a, reg1: :b,
                                 reg0_high_byte?: false,
                                 reg1_high_byte?: true

          assert_disassembles_to 'add sil, dil', inst,
                                 reg0: :si, reg1: :di,
                                 reg0_high_byte?: false,
                                 reg1_high_byte?: false

          assert_disassembles_to 'add sil, dil', inst,
                                 reg0: :si, reg1: :di


          assert_raises Evoasm::Error do
            Evoasm::X64.encode :add_r8_rm8,
                               reg0: :si,
                               reg1: :b,
                               reg1_high_byte?: true
          end
        end

        assert_assembles_to "\x40\xb6\x10", :mov_r8_imm8,
                            reg0: :si, imm0: 0x10

        assert_disassembles_to 'mov sil, 0x10', :mov_r8_imm8,
                               reg0: :si, imm0: 0x10

        assert_disassembles_to 'movsx ax, dl', :movsx_r16_rm8,
                               reg0: :a, reg1: :d, reg1_high_byte?: false

        assert_disassembles_to 'movsx ax, dh', :movsx_r16_rm8,
                               reg0: :a, reg1: :d, reg1_high_byte?: true

      end

      def test_imm
        assert_disassembles_to 'add rax, 0xa', :add_rm64_imm8,
                               reg0: :a, imm0: 0xa

        assert_disassembles_to 'add qword ptr [rax], 0xa', :add_rm64_imm8,
                               reg_base: :a, imm0: 0xa

        assert_disassembles_to 'add qword ptr [rax], -0xa', :add_rm64_imm8,
                               reg_base: :a, imm0: -0xa

        assert_raises Evoasm::Error do |e|
          assemble :add_rm64_imm8, reg0: :a, imm0: 0xfff
        end

        assert_raises Evoasm::Error do |e|
          assemble :add_rm64_imm8, reg0: :a, imm0: -0xfff
        end
      end

      def test_vex
        assert_assembles_to "\xC5\xF8\x5A\xCA", :vcvtps2pd_xmm_xmmm64,
                            reg0: :xmm1, reg1: :xmm2
        assert_assembles_to "\xC5\xEA\x5A\xCB", :vcvtss2sd_xmm_xmm_xmmm32,
                            reg0: :xmm1, reg1: :xmm2, reg2: :xmm3
        assert_assembles_to "\xC4\xE3\x79\x1D\xD1\x00", :vcvtps2ph_xmmm64_xmm_imm8,
                            reg0: :xmm1, reg1: :xmm2, imm0: 0x0
        assert_assembles_to "\xC4\xC1\xFA\x2C\xFD", :vcvttss2si_r64_xmmm32,
                            reg0: :di, reg1: :xmm13

        (0..15).each do |i|
          assert_disassembles_to "vcvttss2si rdi, xmm#{i}", :vcvttss2si_r64_xmmm32,
                                 reg0: :di, reg1: :"xmm#{i}"

          assert_disassembles_to "vcvttss2si r12, xmm#{i}", :vcvttss2si_r64_xmmm32,
                                 reg0: :r12, reg1: :"xmm#{i}"
        end

        assert_assembles_to "\xC5\xF5\xEC\xC2", :vpaddsb_ymm_ymm_ymmm256,
                            reg0: :xmm0, reg1: :xmm1, reg2: :xmm2, force_long_vex?: false
        assert_assembles_to "\xC5\xF5\xEC\xC2", :vpaddsb_ymm_ymm_ymmm256,
                            reg0: :xmm0, reg1: :xmm1, reg2: :xmm2
        assert_assembles_to "\xC4\xE1\x75\xEC\xC2", :vpaddsb_ymm_ymm_ymmm256,
                            reg0: :xmm0, reg1: :xmm1, reg2: :xmm2, force_long_vex?: true

        assert_disassembles_to 'vpaddsb ymm0, ymm1, ymm2', :vpaddsb_ymm_ymm_ymmm256,
                               reg0: :xmm0, reg1: :xmm1, reg2: :xmm2
        assert_disassembles_to 'vpaddsb ymm0, ymm1, ymm2', :vpaddsb_ymm_ymm_ymmm256,
                               reg0: :xmm0, reg1: :xmm1, reg2: :xmm2, force_long_vex?: false
        assert_disassembles_to 'vpaddsb ymm0, ymm1, ymm2', :vpaddsb_ymm_ymm_ymmm256,
                               reg0: :xmm0, reg1: :xmm1, reg2: :xmm2, force_long_vex?: true

        assert_disassembles_to 'vblendvpd xmm0, xmm1, xmm2, xmm3', :vblendvpd_xmm_xmm_xmmm128_xmm,
                               reg0: :xmm0, reg1: :xmm1, reg2: :xmm2, reg3: :xmm3
      end

      def test_vsib
        assert_disassembles_to 'vpgatherdd xmm0, dword ptr [rax + xmm1*2], xmm2', :vpgatherdd_xmm_vm32x32_xmm,
                               reg0: :xmm0, reg_base: :a, reg_index: :xmm1, scale: 2, reg1: :xmm2

        assert_disassembles_to 'vpgatherdd xmm0, dword ptr [rax + xmm1], xmm2', :vpgatherdd_xmm_vm32x32_xmm,
                               reg0: :xmm0, reg_base: :a, reg_index: :xmm1, scale: 1, reg1: :xmm2

        assert_disassembles_to 'vpgatherdd xmm12, dword ptr [rax + xmm1], xmm2', :vpgatherdd_xmm_vm32x32_xmm,
                               reg0: :xmm12, reg_base: :a, reg_index: :xmm1, scale: 1, reg1: :xmm2

        assert_disassembles_to 'vpgatherdd xmm12, dword ptr [r12 + xmm1], xmm2', :vpgatherdd_xmm_vm32x32_xmm,
                               reg0: :xmm12, reg_base: :r12, reg_index: :xmm1, scale: 1, reg1: :xmm2

        assert_disassembles_to 'vpgatherdd xmm12, dword ptr [r12 + xmm12*8], xmm2', :vpgatherdd_xmm_vm32x32_xmm,
                               reg0: :xmm12, reg_base: :r12, reg_index: :xmm12, scale: 8, reg1: :xmm2

        assert_disassembles_to 'vpgatherdd xmm12, dword ptr [rdi + xmm12*8], xmm2', :vpgatherdd_xmm_vm32x32_xmm,
                               reg0: :xmm12, reg_base: :di, reg_index: :xmm12, scale: 8, reg1: :xmm2

        assert_disassembles_to 'vpgatherqq ymm12, qword ptr [rdi + ymm12*8], ymm2', :vpgatherqq_ymm_vm64y64_ymm,
                               reg0: :xmm12, reg_base: :di, reg_index: :xmm12, scale: 8, reg1: :xmm2

      end
    end
  end
end
