// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only

package relayer

import (
	"context"
	"crypto/ecdsa"
	"fmt"
	"math/big"
	"testing"

	ethcommon "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/stretchr/testify/require"

	"github.com/athanorlabs/atomic-swap/coins"
	"github.com/athanorlabs/atomic-swap/common/types"
	contracts "github.com/athanorlabs/atomic-swap/ethereum"
	"github.com/athanorlabs/atomic-swap/net/message"
	"github.com/athanorlabs/atomic-swap/tests"
)

func TestValidateRelayerFee(t *testing.T) {
	ctx := context.Background()
	ec, _ := tests.NewEthClient(t)
	key := tests.GetTakerTestKey(t)
	swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, key)

	// 20-byte empty address, 4-byte zero salt
	empty := [24]byte{}
	relayerHash := crypto.Keccak256Hash(empty[:])

	type testCase struct {
		description string
		value       *big.Int
		expectErr   string
	}

	testCases := []testCase{
		{
			description: "swap value equal to relayer fee",
			value:       coins.RelayerFeeWei,
			expectErr:   "swap value of 0.01 ETH is too low to support 0.01 ETH relayer fee",
		},
		{
			description: "swap value less than relayer fee",
			value:       new(big.Int).Sub(coins.RelayerFeeWei, big.NewInt(1e15)),
			expectErr:   "swap value of 0.009 ETH is too low to support 0.01 ETH relayer fee",
		},
		{
			description: "swap value larger than min fee",
			value:       new(big.Int).Add(coins.RelayerFeeWei, big.NewInt(1e15)),
		},
	}

	for _, tc := range testCases {
		swap := contracts.SwapCreatorSwap{
			Owner:            ethcommon.Address{},
			Claimer:          ethcommon.Address{},
			ClaimCommitment:  [32]byte{},
			RefundCommitment: [32]byte{},
			Timeout1:         new(big.Int),
			Timeout2:         new(big.Int),
			Asset:            ethcommon.Address{},
			Value:            tc.value,
			Nonce:            new(big.Int),
		}

		request := &message.RelayClaimRequest{
			RelaySwap: &contracts.SwapCreatorRelaySwap{
				Swap:        swap,
				Fee:         coins.RelayerFeeWei,
				SwapCreator: swapCreatorAddr,
				RelayerHash: relayerHash,
			},
			Secret: make([]byte, 32),
		}

		err := validateClaimValues(ctx, request, ec, ethcommon.Address{}, [4]byte{}, swapCreatorAddr)
		if tc.expectErr != "" {
			require.ErrorContains(t, err, tc.expectErr, tc.description)
		} else {
			require.NoError(t, err, tc.description)
		}
	}
}

// In the taker claim scenario, we need to fail if the contract address is not
// identical. If the claim has a different address, the swap was not created by
// the taker who is being asked to claim.
func Test_validateClaimValues_takerClaim_contractAddressNotEqualFail(t *testing.T) {
	offerID := types.Hash{0x1}                       // non-nil offer ID passed to indicate taker claim
	swapCreatorAddrInClaim := ethcommon.Address{0x1} // address in claim
	swapCreatorAddrOurs := ethcommon.Address{0x2}    // passed to validateClaimValues

	request := &message.RelayClaimRequest{
		OfferID: &offerID,
		Secret:  make([]byte, 32),
		RelaySwap: &contracts.SwapCreatorRelaySwap{
			SwapCreator: swapCreatorAddrInClaim,
		},
	}

	err := validateClaimValues(context.Background(), request, nil, ethcommon.Address{}, [4]byte{}, swapCreatorAddrOurs)
	require.ErrorContains(t, err, "taker claim swap creator mismatch")
}

// When validating a claim made to a DHT advertised relayer, the contacts can have
// different addresses, but the claim's contract must be byte-code compatible. This
// tests for failure when it is not byte-code compatible.
func Test_validateClaimValues_dhtClaim_contractAddressNotEqual(t *testing.T) {
	ec, _ := tests.NewEthClient(t)
	key := tests.GetTakerTestKey(t)
	swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, key)

	request := &message.RelayClaimRequest{
		OfferID: nil, // DHT relayer claim
		Secret:  make([]byte, 32),
		RelaySwap: &contracts.SwapCreatorRelaySwap{
			SwapCreator: ethcommon.Address{1}, // not a valid swap creator contract
		},
	}

	err := validateClaimValues(context.Background(), request, ec, ethcommon.Address{}, [4]byte{}, swapCreatorAddr)
	require.ErrorContains(t, err, "contract address does not contain correct SwapCreator code")
}

func Test_validateSignature(t *testing.T) {
	ethKey := tests.GetMakerTestKey(t)
	claimer := crypto.PubkeyToAddress(*ethKey.Public().(*ecdsa.PublicKey))
	ec, _ := tests.NewEthClient(t)
	secret := [32]byte{0x1}
	swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, ethKey)

	swap := createTestSwap(claimer)
	relaySwap := &contracts.SwapCreatorRelaySwap{
		SwapCreator: swapCreatorAddr,
		Swap:        *swap,
		RelayerHash: types.Hash{},
		Fee:         big.NewInt(1),
	}

	req, err := CreateRelayClaimRequest(ethKey, relaySwap, secret)
	require.NoError(t, err)

	// success path
	err = validateClaimSignature(req)
	require.NoError(t, err)

	// failure path (tamper with an arbitrary byte of the signature)
	req.Signature[10]++
	err = validateClaimSignature(req)
	// can be "recovery failed" or "signer of message is not swap claimer"
	require.Error(t, err)
}

func Test_validateClaimRequest(t *testing.T) {
	ctx := context.Background()
	ethKey := tests.GetMakerTestKey(t)
	claimer := crypto.PubkeyToAddress(*ethKey.Public().(*ecdsa.PublicKey))
	ec, _ := tests.NewEthClient(t)
	secret := [32]byte{0x1}
	swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, ethKey)

	// 20-byte empty address, 4-byte zero salt
	empty := [24]byte{}
	relayerHash := crypto.Keccak256Hash(empty[:])

	swap := createTestSwap(claimer)
	relaySwap := &contracts.SwapCreatorRelaySwap{
		SwapCreator: swapCreatorAddr,
		Swap:        *swap,
		RelayerHash: relayerHash,
		Fee:         coins.RelayerFeeWei,
	}

	req, err := CreateRelayClaimRequest(ethKey, relaySwap, secret)
	require.NoError(t, err)

	// success path
	err = validateClaimRequest(ctx, req, ec, ethcommon.Address{}, [4]byte{}, swapCreatorAddr)
	require.NoError(t, err)

	// test failure path by passing a non-eth asset
	asset := ethcommon.Address{0x1}
	req.RelaySwap.Swap.Asset = asset
	err = validateClaimRequest(ctx, req, ec, ethcommon.Address{}, [4]byte{}, swapCreatorAddr)
	require.ErrorContains(t, err, fmt.Sprintf("relaying for ETH Asset %s is not supported", types.EthAsset(asset)))
}
