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

package stdscript

import (
	"bytes"
	"fmt"
	"testing"

	"github.com/decred/dcrd/txscript/v4"
)

// These variables are used to help ensure the benchmarks do not elide code.
var (
	noElideSwapData *AtomicSwapDataPushesV0
)

// complexScriptV0 is a version 0 script comprised of half as many opcodes as
// the maximum allowed followed by as many max size data pushes fit without
// exceeding the max allowed script size.
var complexScriptV0 = func() []byte {
	const (
		maxScriptSize        = txscript.MaxScriptSize
		maxScriptElementSize = txscript.MaxScriptElementSize
	)
	var scriptLen int
	builder := txscript.NewScriptBuilder()
	for i := 0; i < txscript.MaxOpsPerScript/2; i++ {
		builder.AddOp(txscript.OP_TRUE)
		scriptLen++
	}
	maxData := bytes.Repeat([]byte{0x02}, maxScriptElementSize)
	for i := 0; i < (maxScriptSize-scriptLen)/maxScriptElementSize; i++ {
		builder.AddData(maxData)
	}
	script, err := builder.Script()
	if err != nil {
		panic(err)
	}
	return script
}()

// makeBenchmarks constructs a slice of tests to use in the benchmarks as
// follows:
// - Start with a version 0 complex non standard script
// - Add all tests for which the provided filter function returns true
func makeBenchmarks(filterFn func(test scriptTest) bool) []scriptTest {
	benches := make([]scriptTest, 0, 5)
	benches = append(benches, scriptTest{
		name:    "v0 complex non standard",
		version: 0,
		script:  complexScriptV0,
	})
	for _, test := range scriptV0Tests {
		if filterFn(test) {
			benches = append(benches, test)
		}
	}
	return benches
}

// benchIsX is a convenience function that runs benchmarks for the entries that
// match the provided filter function using the given script type determination
// function and ensures the result matches the expected one.
func benchIsX(b *testing.B, filterFn func(test scriptTest) bool, isXFn func(scriptVersion uint16, script []byte) bool) {
	b.Helper()

	benches := makeBenchmarks(filterFn)
	for _, bench := range benches {
		want := filterFn(bench)
		b.Run(bench.name, func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				got := isXFn(bench.version, bench.script)
				if got != want {
					b.Fatalf("%q: unexpected result -- got %v, want %v",
						bench.name, got, want)
				}
			}
		})
	}
}

// BenchmarkIsPubKeyScript benchmarks the performance of analyzing various
// public key scripts to determine if they are p2pk-ecdsa-secp256k1 scripts.
func BenchmarkIsPubKeyScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STPubKeyEcdsaSecp256k1
	}
	benchIsX(b, filterFn, IsPubKeyScript)
}

// BenchmarkIsPubKeyEd25519Script benchmarks the performance of analyzing
// various public key scripts to determine if they are p2pkh-ed25519 scripts.
func BenchmarkIsPubKeyEd25519Script(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STPubKeyEd25519
	}
	benchIsX(b, filterFn, IsPubKeyEd25519Script)
}

// BenchmarkIsPubKeySchnorrSecp256k1Script benchmarks the performance of
// analyzing various public key scripts to determine if they are
// p2pkh-schnorr-secp256k1 scripts.
func BenchmarkIsPubKeySchnorrSecp256k1Script(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STPubKeySchnorrSecp256k1
	}
	benchIsX(b, filterFn, IsPubKeySchnorrSecp256k1Script)
}

// BenchmarkIsPubKeyHashScript benchmarks the performance of analyzing various
// public key scripts to determine if they are p2pkh-ecdsa-secp256k1 scripts.
func BenchmarkIsPubKeyHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STPubKeyHashEcdsaSecp256k1
	}
	benchIsX(b, filterFn, IsPubKeyHashScript)
}

// BenchmarkIsPubKeyHashEd25519Script benchmarks the performance of analyzing
// various public key scripts to determine if they are p2pkh-ed25519 scripts.
func BenchmarkIsPubKeyHashEd25519Script(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STPubKeyHashEd25519
	}
	benchIsX(b, filterFn, IsPubKeyHashEd25519Script)
}

// BenchmarkIsPubKeyHashSchnorrSecp256k1Script benchmarks the performance of
// analyzing various public key scripts to determine if they are
// p2pkh-schnorr-secp256k1 scripts.
func BenchmarkIsPubKeyHashSchnorrSecp256k1Script(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STPubKeyHashSchnorrSecp256k1
	}
	benchIsX(b, filterFn, IsPubKeyHashSchnorrSecp256k1Script)
}

// BenchmarkIsScriptHashScript benchmarks the performance of analyzing various
// public key scripts to determine if they are p2sh scripts.
func BenchmarkIsScriptHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STScriptHash
	}
	benchIsX(b, filterFn, IsScriptHashScript)
}

// BenchmarkIsMultiSigScript benchmarks the performance of analyzing various
// public key scripts to determine if they are multisignature scripts.
func BenchmarkIsMultiSigScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STMultiSig && !test.isSig
	}
	benchIsX(b, filterFn, IsMultiSigScript)
}

// BenchmarkIsMultiSigSigScript benchmarks the performance of analyzing various
// signature scripts to determine if they are likely to be multisignature redeem
// scripts.
func BenchmarkIsMultiSigSigScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STMultiSig && test.isSig
	}
	benchIsX(b, filterFn, IsMultiSigSigScript)
}

// BenchmarkMultiSigRedeemScriptFromScriptSigV0 benchmarks the performance of
// extracting the redeem script from various version 0 signature scripts.
func BenchmarkMultiSigRedeemScriptFromScriptSigV0(b *testing.B) {
	for _, test := range scriptV0Tests {
		if test.version != 0 || test.wantType != STMultiSig || !test.isSig {
			continue
		}
		want := test.wantData.([]byte)

		b.Run(test.name, func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				got := MultiSigRedeemScriptFromScriptSigV0(test.script)
				if !bytes.Equal(got, want) {
					b.Fatalf("%q: unexpected result -- got %x, want %x",
						test.name, got, want)
				}
			}
		})
	}
}

// BenchmarkIsNullDataScript benchmarks the performance of analyzing various
// public key scripts to determine if they are nulldata scripts.
func BenchmarkIsNullDataScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STNullData
	}
	benchIsX(b, filterFn, IsNullDataScript)
}

// BenchmarkIsStakeSubmissionPubKeyHashScript benchmarks the performance of
// analyzing various public key scripts to determine if they are stake
// submission p2pkh-ecdsa-secp256k1 scripts.
func BenchmarkIsStakeSubmissionPubKeyHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STStakeSubmissionPubKeyHash
	}
	benchIsX(b, filterFn, IsStakeSubmissionPubKeyHashScript)
}

// BenchmarkIsStakeSubmissionScriptHashScript benchmarks the performance of
// analyzing various public key scripts to determine if they are stake
// submission p2sh scripts.
func BenchmarkIsStakeSubmissionScriptHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STStakeSubmissionScriptHash
	}
	benchIsX(b, filterFn, IsStakeSubmissionScriptHashScript)
}

// BenchmarkIsStakeGenPubKeyHashScript benchmarks the performance of analyzing
// various public key scripts to determine if they are stake generation
// p2pkh-ecdsa-secp256k1 scripts.
func BenchmarkIsStakeGenPubKeyHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STStakeGenPubKeyHash
	}
	benchIsX(b, filterFn, IsStakeGenPubKeyHashScript)
}

// BenchmarkIsStakeGenScriptHashScript benchmarks the performance of analyzing various
// public key scripts to determine if they are stake generation p2sh scripts.
func BenchmarkIsStakeGenScriptHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STStakeGenScriptHash
	}
	benchIsX(b, filterFn, IsStakeGenScriptHashScript)
}

// BenchmarkIsStakeRevocationPubKeyHashScript benchmarks the performance of
// analyzing various public key scripts to determine if they are stake
// revocation p2pkh-ecdsa-secp256k1 scripts.
func BenchmarkIsStakeRevocationPubKeyHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STStakeRevocationPubKeyHash
	}
	benchIsX(b, filterFn, IsStakeRevocationPubKeyHashScript)
}

// BenchmarkIsStakeRevocationScriptHashScript benchmarks the performance of
// various public key scripts to determine if they are stake revocation p2sh
// scripts.
func BenchmarkIsStakeRevocationScriptHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STStakeRevocationScriptHash
	}
	benchIsX(b, filterFn, IsStakeRevocationScriptHashScript)
}

// BenchmarkIsStakeChangePubKeyHash benchmarks the performance of analyzing
// various public key scripts to determine if they are stake change
// p2pkh-ecdsa-secp256k1 scripts.
func BenchmarkIsStakeChangePubKeyHash(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STStakeChangePubKeyHash
	}
	benchIsX(b, filterFn, IsStakeChangePubKeyHashScript)
}

// BenchmarkIsStakeChangeScriptHash benchmarks the performance of analyzing
// various public key scripts to determine if they are stake change p2sh
// scripts.
func BenchmarkIsStakeChangeScriptHash(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STStakeChangeScriptHash
	}
	benchIsX(b, filterFn, IsStakeChangeScriptHashScript)
}

// BenchmarkIsTreasuryAddScript benchmarks the performance of analyzing various
// public key scripts to determine if they are treasury add scripts.
func BenchmarkIsTreasuryAddScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STTreasuryAdd
	}
	benchIsX(b, filterFn, IsTreasuryAddScript)
}

// BenchmarkIsTreasuryGenPubKeyHashScript benchmarks the performance of
// analyzing various public key scripts to determine if they are treasury
// generation p2pkh-ecdsa-secp256k1 scripts.
func BenchmarkIsTreasuryGenPubKeyHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STTreasuryGenPubKeyHash
	}
	benchIsX(b, filterFn, IsTreasuryGenPubKeyHashScript)
}

// BenchmarkIsTreasuryGenScriptHashScript benchmarks the performance of
// analyzing various public key scripts to determine if they are treasury
// generation p2sh scripts.
func BenchmarkIsTreasuryGenScriptHashScript(b *testing.B) {
	filterFn := func(test scriptTest) bool {
		return test.wantType == STTreasuryGenScriptHash
	}
	benchIsX(b, filterFn, IsTreasuryGenScriptHashScript)
}

// BenchmarkDetermineScriptType benchmarks the performance of analyzing various
// public key scripts to determine what type of standard script they are.
func BenchmarkDetermineScriptType(b *testing.B) {
	counts := make(map[ScriptType]int)
	benches := makeBenchmarks(func(test scriptTest) bool {
		// Limit to one of each script type.
		counts[test.wantType]++
		return test.wantType != STNonStandard && counts[test.wantType] == 1
	})

	for _, bench := range benches {
		b.Run(bench.name, func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				got := DetermineScriptType(bench.version, bench.script)
				if got != bench.wantType {
					b.Fatalf("%q: unexpected result -- got %v, want %v",
						bench.name, got, bench.wantType)
				}
			}
		})
	}
}

// BenchmarkDetermineRequiredSigs benchmarks the performance of determining the
// required number of signatures for various public key scripts.
func BenchmarkDetermineRequiredSigs(b *testing.B) {
	counts := make(map[ScriptType]int)
	benches := makeBenchmarks(func(test scriptTest) bool {
		// Limit to one of each script type.
		counts[test.wantType]++
		return counts[test.wantType] == 1
	})

	for _, bench := range benches {
		b.Run(bench.name, func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				got := DetermineRequiredSigs(bench.version, bench.script)
				if got != bench.wantSigs {
					b.Fatalf("%q: unexpected result -- got %v, want %v",
						bench.name, got, bench.wantSigs)
				}
			}
		})
	}
}

// BenchmarkExtractAtomicSwapDataPushes benchmarks the performance of
// attempting to extract the atomic swap data pushes from various version 0
// public key scripts.
func BenchmarkExtractAtomicSwapDataPushes(b *testing.B) {
	benches := []struct {
		name   string // benchmark name
		script []byte // script to analyze
	}{{
		name:   "v0 complex not atomic swap",
		script: complexScriptV0,
	}, {
		name: "v0 normal valid atomic swap",
		script: mustParseShortForm(0, fmt.Sprintf("IF "+
			"SIZE 32 EQUALVERIFY "+
			"SHA256 DATA_32 "+
			"0x9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 "+
			"EQUALVERIFY DUP HASH160 "+
			"DATA_20 0x0000000000000000000000000000000000000001 "+
			"ELSE "+
			"300000 CHECKLOCKTIMEVERIFY DROP DUP HASH160 "+
			"DATA_20 0x0000000000000000000000000000000000000002 "+
			"ENDIF "+
			"EQUALVERIFY CHECKSIG")),
	}}

	for _, bench := range benches {
		b.Run(bench.name, func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				noElideSwapData = ExtractAtomicSwapDataPushesV0(bench.script)
			}
		})
	}
}
