import os

from scripts.test import shared
from . import utils


class FeatureValidationTest(utils.BinaryenTestCase):
    def check_feature(self, module, error, flag):
        p = shared.run_process(shared.WASM_OPT +
                               ['--mvp-features', '--print', '-o', os.devnull],
                               input=module, check=False, capture_output=True)
        self.assertIn(error, p.stderr)
        self.assertIn('Fatal: error validating input', p.stderr)
        self.assertNotEqual(p.returncode, 0)
        p = shared.run_process(
            shared.WASM_OPT + ['--mvp-features', '--print', '-o', os.devnull] +
            flag,
            input=module,
            check=False,
            capture_output=True)
        self.assertEqual(p.returncode, 0)

    def check_simd(self, module, error):
        self.check_feature(module, error, ['--enable-simd'])

    def check_sign_ext(self, module, error):
        self.check_feature(module, error, ['--enable-sign-ext'])

    def check_bulk_mem(self, module, error):
        self.check_feature(module, error, ['--enable-bulk-memory'])

    def check_exception_handling(self, module, error):
        # Exception handling implies reference types
        self.check_feature(module, error,
                           ['--enable-reference-types',
                            '--enable-exception-handling'])

    def check_tail_call(self, module, error):
        self.check_feature(module, error, ['--enable-tail-call'])

    def check_reference_types(self, module, error):
        self.check_feature(module, error, ['--enable-reference-types'])

    def test_v128_signature(self):
        module = '''
        (module
         (func $foo (param $0 v128) (result v128)
            (local.get $0)
         )
        )
        '''
        self.check_simd(module, 'all used types should be allowed')

    def test_v128_global(self):
        module = '''
        (module
         (global $foo (mut v128) (v128.const i32x4 0 0 0 0))
        )
        '''
        self.check_simd(module, 'all used types should be allowed')

    def test_v128_local(self):
        module = '''
        (module
         (func $foo
            (local v128)
         )
        )
        '''
        self.check_simd(module, 'all used types should be allowed')

    def test_simd_const(self):
        module = '''
        (module
         (func $foo
            (drop (v128.const i32x4 0 0 0 0))
         )
        )
        '''
        self.check_simd(module, 'all used features should be allowed')

    def test_simd_load(self):
        module = '''
        (module
         (memory 1 1)
         (func $foo
            (drop (v128.load (i32.const 0)))
         )
        )
        '''
        self.check_simd(module, 'SIMD operation (SIMD is disabled)')

    def test_simd_splat(self):
        module = '''
        (module
         (func $foo
            (drop (i32x4.splat (i32.const 0)))
         )
        )
        '''
        self.check_simd(module, 'all used features should be allowed')

    def test_sign_ext(self):
        module = '''
        (module
         (func $foo
            (drop (i32.extend8_s (i32.const 7)))
         )
        )
        '''
        self.check_sign_ext(module, 'all used features should be allowed')

    def test_bulk_mem_inst(self):
        module = '''
        (module
         (memory 1 1)
         (func $foo
            (memory.copy (i32.const 0) (i32.const 8) (i32.const 8))
         )
        )
        '''
        self.check_bulk_mem(module,
                            'Bulk memory operation (bulk memory is disabled')

    def test_bulk_mem_segment(self):
        module = '''
        (module
         (memory 256 256)
         (data passive "42")
        )
        '''
        self.check_bulk_mem(module, 'nonzero segment flags (bulk memory is disabled)')

    def test_tail_call(self):
        module = '''
        (module
         (func $bar)
         (func $foo
            (return_call $bar)
         )
        )
        '''
        self.check_tail_call(module, 'return_call requires tail calls to be enabled')

    def test_tail_call_indirect(self):
        module = '''
        (module
         (type $T (func))
         (table $0 1 1 funcref)
         (func $foo
            (return_call_indirect (type $T)
             (i32.const 0)
            )
         )
        )
        '''
        self.check_tail_call(module, 'return_call_indirect requires tail calls to be enabled')

    def test_reference_types_anyref(self):
        module = '''
        (module
         (import "env" "test1" (func $test1 (param anyref) (result anyref)))
         (import "env" "test2" (global $test2 anyref))
         (export "test1" (func $test1 (param anyref) (result anyref)))
         (export "test2" (global $test2))
         (func $anyref_test (param $0 anyref) (result anyref)
          (return
           (call $test1
            (local.get $0)
           )
          )
         )
        )
        '''
        self.check_reference_types(module, 'all used types should be allowed')

    def test_exnref_local(self):
        module = '''
        (module
         (func $foo
            (local exnref)
         )
        )
        '''
        self.check_exception_handling(module, 'all used types should be allowed')

    def test_event(self):
        module = '''
        (module
         (event $e (attr 0) (param i32))
         (func $foo
            (throw $e (i32.const 0))
         )
        )
        '''
        self.check_exception_handling(module, 'Module has events')


class TargetFeaturesSectionTest(utils.BinaryenTestCase):
    def test_atomics(self):
        filename = 'atomics_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['threads'])
        self.assertIn('i32.atomic.rmw.add', self.disassemble(filename))

    def test_bulk_memory(self):
        filename = 'bulkmem_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['bulk-memory'])
        self.assertIn('memory.copy', self.disassemble(filename))

    def test_nontrapping_fptoint(self):
        filename = 'truncsat_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['nontrapping-float-to-int'])
        self.assertIn('i32.trunc_sat_f32_u', self.disassemble(filename))

    def test_mutable_globals(self):
        filename = 'mutable_globals_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['mutable-globals'])
        self.assertIn('(import "env" "global-mut" (global $gimport$0 (mut i32)))',
                      self.disassemble(filename))

    def test_sign_ext(self):
        filename = 'signext_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['sign-ext'])
        self.assertIn('i32.extend8_s', self.disassemble(filename))

    def test_simd(self):
        filename = 'simd_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['simd'])
        self.assertIn('i32x4.splat', self.disassemble(filename))

    def test_tailcall(self):
        filename = 'tail_call_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['tail-call'])
        self.assertIn('return_call', self.disassemble(filename))

    def test_reference_types(self):
        filename = 'reference_types_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['reference-types'])
        self.assertIn('anyref', self.disassemble(filename))

    def test_exception_handling(self):
        filename = 'exception_handling_target_feature.wasm'
        self.roundtrip(filename)
        self.check_features(filename, ['exception-handling'])
        self.assertIn('throw', self.disassemble(filename))

    def test_incompatible_features(self):
        path = self.input_path('signext_target_feature.wasm')
        p = shared.run_process(
            shared.WASM_OPT + ['--print', '--enable-simd', '-o', os.devnull,
                               path],
            check=False, capture_output=True
        )
        self.assertNotEqual(p.returncode, 0)
        self.assertIn('Fatal: module features do not match specified features. ' +
                      'Use --detect-features to resolve.',
                      p.stderr)

    def test_incompatible_features_forced(self):
        path = self.input_path('signext_target_feature.wasm')
        p = shared.run_process(
            shared.WASM_OPT + ['--print', '--detect-features', '-mvp',
                               '--enable-simd', '-o', os.devnull, path],
            check=False, capture_output=True
        )
        self.assertNotEqual(p.returncode, 0)
        self.assertIn('all used features should be allowed', p.stderr)

    def test_explicit_detect_features(self):
        self.check_features('signext_target_feature.wasm', ['sign-ext', 'simd'],
                            opts=['-mvp', '--detect-features', '--enable-simd'])

    def test_emit_all_features(self):
        p = shared.run_process(shared.WASM_OPT +
                               ['--emit-target-features', '-all', '-o', '-'],
                               input="(module)", check=False,
                               capture_output=True, decode_output=False)
        self.assertEqual(p.returncode, 0)
        p2 = shared.run_process(shared.WASM_OPT +
                                ['--print-features', '-o', os.devnull],
                                input=p.stdout, check=False,
                                capture_output=True)
        self.assertEqual(p2.returncode, 0)
        self.assertEqual([
            '--enable-threads',
            '--enable-bulk-memory',
            '--enable-exception-handling',
            '--enable-mutable-globals',
            '--enable-nontrapping-float-to-int',
            '--enable-sign-ext',
            '--enable-simd',
            '--enable-tail-call',
            '--enable-reference-types'
        ], p2.stdout.split())
