// Copyright (c) 2019-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package txscript

import (
	"bytes"
	"errors"
	"fmt"
	"testing"
)

// TestScriptTokenizer ensures a wide variety of behavior provided by the script
// tokenizer performs as expected.
func TestScriptTokenizer(t *testing.T) {
	type expectedResult struct {
		op    byte   // expected parsed opcode
		data  []byte // expected parsed data
		index int32  // expected index into raw script after parsing token
	}

	type tokenizerTest struct {
		name     string           // test description
		script   []byte           // the script to tokenize
		expected []expectedResult // the expected info after parsing each token
		finalIdx int32            // the expected final byte index
		err      error            // expected error
	}

	// Add both positive and negative tests for OP_DATA_1 through OP_DATA_75.
	const numTestsHint = 100 // Make prealloc linter happy.
	tests := make([]tokenizerTest, 0, numTestsHint)
	for op := byte(OP_DATA_1); op < OP_DATA_75; op++ {
		data := bytes.Repeat([]byte{0x01}, int(op))
		tests = append(tests, tokenizerTest{
			name:     fmt.Sprintf("OP_DATA_%d", op),
			script:   append([]byte{op}, data...),
			expected: []expectedResult{{op, data, 1 + int32(op)}},
			finalIdx: 1 + int32(op),
			err:      nil,
		})

		// Create test that provides one less byte than the data push requires.
		tests = append(tests, tokenizerTest{
			name:     fmt.Sprintf("short OP_DATA_%d", op),
			script:   append([]byte{op}, data[1:]...),
			expected: nil,
			finalIdx: 0,
			err:      ErrMalformedPush,
		})
	}

	// Add both positive and negative tests for OP_PUSHDATA{1,2,4}.
	data := mustParseShortFormV0("0x01{76}")
	tests = append(tests, []tokenizerTest{{
		name:     "OP_PUSHDATA1",
		script:   mustParseShortFormV0("OP_PUSHDATA1 0x4c 0x01{76}"),
		expected: []expectedResult{{OP_PUSHDATA1, data, 2 + int32(len(data))}},
		finalIdx: 2 + int32(len(data)),
		err:      nil,
	}, {
		name:     "OP_PUSHDATA1 no data length",
		script:   mustParseShortFormV0("OP_PUSHDATA1"),
		expected: nil,
		finalIdx: 0,
		err:      ErrMalformedPush,
	}, {
		name:     "OP_PUSHDATA1 short data by 1 byte",
		script:   mustParseShortFormV0("OP_PUSHDATA1 0x4c 0x01{75}"),
		expected: nil,
		finalIdx: 0,
		err:      ErrMalformedPush,
	}, {
		name:     "OP_PUSHDATA2",
		script:   mustParseShortFormV0("OP_PUSHDATA2 0x4c00 0x01{76}"),
		expected: []expectedResult{{OP_PUSHDATA2, data, 3 + int32(len(data))}},
		finalIdx: 3 + int32(len(data)),
		err:      nil,
	}, {
		name:     "OP_PUSHDATA2 no data length",
		script:   mustParseShortFormV0("OP_PUSHDATA2"),
		expected: nil,
		finalIdx: 0,
		err:      ErrMalformedPush,
	}, {
		name:     "OP_PUSHDATA2 short data by 1 byte",
		script:   mustParseShortFormV0("OP_PUSHDATA2 0x4c00 0x01{75}"),
		expected: nil,
		finalIdx: 0,
		err:      ErrMalformedPush,
	}, {
		name:     "OP_PUSHDATA4",
		script:   mustParseShortFormV0("OP_PUSHDATA4 0x4c000000 0x01{76}"),
		expected: []expectedResult{{OP_PUSHDATA4, data, 5 + int32(len(data))}},
		finalIdx: 5 + int32(len(data)),
		err:      nil,
	}, {
		name:     "OP_PUSHDATA4 no data length",
		script:   mustParseShortFormV0("OP_PUSHDATA4"),
		expected: nil,
		finalIdx: 0,
		err:      ErrMalformedPush,
	}, {
		name:     "OP_PUSHDATA4 short data by 1 byte",
		script:   mustParseShortFormV0("OP_PUSHDATA4 0x4c000000 0x01{75}"),
		expected: nil,
		finalIdx: 0,
		err:      ErrMalformedPush,
	}}...)

	// Add tests for OP_0, and OP_1 through OP_16 (small integers/true/false).
	opcodes := []byte{OP_0}
	for op := byte(OP_1); op < OP_16; op++ {
		opcodes = append(opcodes, op)
	}
	for _, op := range opcodes {
		tests = append(tests, tokenizerTest{
			name:     fmt.Sprintf("OP_%d", op),
			script:   []byte{op},
			expected: []expectedResult{{op, nil, 1}},
			finalIdx: 1,
			err:      nil,
		})
	}

	// Add various positive and negative tests for multi-opcode scripts.
	tests = append(tests, []tokenizerTest{{
		name:   "pay-to-pubkey-hash",
		script: mustParseShortFormV0("DUP HASH160 DATA_20 0x01{20} EQUAL CHECKSIG"),
		expected: []expectedResult{
			{OP_DUP, nil, 1}, {OP_HASH160, nil, 2},
			{OP_DATA_20, mustParseShortFormV0("0x01{20}"), 23},
			{OP_EQUAL, nil, 24}, {OP_CHECKSIG, nil, 25},
		},
		finalIdx: 25,
		err:      nil,
	}, {
		name:   "almost pay-to-pubkey-hash (short data)",
		script: mustParseShortFormV0("DUP HASH160 DATA_20 0x01{17} EQUAL CHECKSIG"),
		expected: []expectedResult{
			{OP_DUP, nil, 1}, {OP_HASH160, nil, 2},
		},
		finalIdx: 2,
		err:      ErrMalformedPush,
	}, {
		name:   "almost pay-to-pubkey-hash (overlapped data)",
		script: mustParseShortFormV0("DUP HASH160 DATA_20 0x01{19} EQUAL CHECKSIG"),
		expected: []expectedResult{
			{OP_DUP, nil, 1}, {OP_HASH160, nil, 2},
			{OP_DATA_20, mustParseShortFormV0("0x01{19} EQUAL"), 23},
			{OP_CHECKSIG, nil, 24},
		},
		finalIdx: 24,
		err:      nil,
	}, {
		name:   "pay-to-script-hash",
		script: mustParseShortFormV0("HASH160 DATA_20 0x01{20} EQUAL"),
		expected: []expectedResult{
			{OP_HASH160, nil, 1},
			{OP_DATA_20, mustParseShortFormV0("0x01{20}"), 22},
			{OP_EQUAL, nil, 23},
		},
		finalIdx: 23,
		err:      nil,
	}, {
		name:   "almost pay-to-script-hash (short data)",
		script: mustParseShortFormV0("HASH160 DATA_20 0x01{18} EQUAL"),
		expected: []expectedResult{
			{OP_HASH160, nil, 1},
		},
		finalIdx: 1,
		err:      ErrMalformedPush,
	}, {
		name:   "almost pay-to-script-hash (overlapped data)",
		script: mustParseShortFormV0("HASH160 DATA_20 0x01{19} EQUAL"),
		expected: []expectedResult{
			{OP_HASH160, nil, 1},
			{OP_DATA_20, mustParseShortFormV0("0x01{19} EQUAL"), 22},
		},
		finalIdx: 22,
		err:      nil,
	}}...)

	const scriptVersion = 0
	for _, test := range tests {
		tokenizer := MakeScriptTokenizer(scriptVersion, test.script)
		var opcodeNum int
		for tokenizer.Next() {
			// Ensure Next never returns true when there is an error set.
			if err := tokenizer.Err(); err != nil {
				t.Fatalf("%q: Next returned true when tokenizer has err: %v",
					test.name, err)
			}

			// Ensure the test data expects a token to be parsed.
			op := tokenizer.Opcode()
			data := tokenizer.Data()
			if opcodeNum >= len(test.expected) {
				t.Fatalf("%q: unexpected token '%d' (data: '%x')", test.name,
					op, data)
			}
			expected := &test.expected[opcodeNum]

			// Ensure the opcode and data are the expected values.
			if op != expected.op {
				t.Fatalf("%q: unexpected opcode -- got %v, want %v", test.name,
					op, expected.op)
			}
			if !bytes.Equal(data, expected.data) {
				t.Fatalf("%q: unexpected data -- got %x, want %x", test.name,
					data, expected.data)
			}

			tokenizerIdx := tokenizer.ByteIndex()
			if tokenizerIdx != expected.index {
				t.Fatalf("%q: unexpected byte index -- got %d, want %d",
					test.name, tokenizerIdx, expected.index)
			}

			opcodeNum++
		}

		// Ensure the tokenizer claims it is done.  This should be the case
		// regardless of whether or not there was a parse error.
		if !tokenizer.Done() {
			t.Fatalf("%q: tokenizer claims it is not done", test.name)
		}

		// Ensure the error is as expected.
		if !errors.Is(tokenizer.Err(), test.err) {
			t.Fatalf("%q: unexpected tokenizer err -- got %v, want %v",
				test.name, tokenizer.Err(), test.err)
		}

		// Ensure the final index is the expected value.
		tokenizerIdx := tokenizer.ByteIndex()
		if tokenizerIdx != test.finalIdx {
			t.Fatalf("%q: unexpected final byte index -- got %d, want %d",
				test.name, tokenizerIdx, test.finalIdx)
		}
	}
}

// TestScriptTokenizerUnsupportedVersion ensures the tokenizer fails immediately
// with an unsupported script version.
func TestScriptTokenizerUnsupportedVersion(t *testing.T) {
	const scriptVersion = 65535
	tokenizer := MakeScriptTokenizer(scriptVersion, nil)
	if !errors.Is(tokenizer.Err(), ErrUnsupportedScriptVersion) {
		t.Fatalf("script tokenizer did not error with unsupported version")
	}
}
