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

//go:build !prod

package monero

//
// This file is only for test support when working with monerod in regtest mode. Use the build
// tag "prod" to prevent symbols in this file from consuming space (or mentioning mining) in
// production binaries.
//

import (
	"context"
	"path"
	"runtime"
	"sync"
	"testing"
	"time"

	"github.com/MarinX/monerorpc"
	"github.com/MarinX/monerorpc/daemon"
	"github.com/MarinX/monerorpc/wallet"
	"github.com/stretchr/testify/require"

	"github.com/athanorlabs/atomic-swap/coins"
	"github.com/athanorlabs/atomic-swap/common"
)

const (
	// Mastering monero example address (we don't use the background mining block rewards in tests)
	blockRewardAddress = "4BKjy1uVRTPiz4pHyaXXawb82XpzLiowSDd8rEQJGqvN6AD6kWosLQ6VJXW9sghopxXgQSh1RTd54JdvvCRsXiF41xvfeW5"
)

// GetWalletRPCDirectory returns the directory path of monero-wallet-rpc.
func GetWalletRPCDirectory(t *testing.T) string {
	_, filename, _, ok := runtime.Caller(0) // this test file path
	require.True(t, ok)
	packageDir := path.Dir(filename)
	repoBaseDir := path.Dir(packageDir)
	return path.Join(repoBaseDir, "monero-bin", "monero-wallet-rpc")
}

// CreateWalletClientWithWalletDir creates a WalletClient with the given wallet directory.
func CreateWalletClientWithWalletDir(t *testing.T, walletDir string) WalletClient {
	moneroWalletRPCPath := GetWalletRPCDirectory(t)
	c, err := NewWalletClient(&WalletClientConf{
		Env:                 common.Development,
		WalletFilePath:      path.Join(walletDir, "test-wallet"),
		MoneroWalletRPCPath: moneroWalletRPCPath,
	})
	require.NoError(t, err)

	t.Cleanup(func() {
		c.Close()
	})
	TestBackgroundMineBlocks(t)
	return c
}

// CreateWalletClient starts a monero-wallet-rpc listening on a random port for tests and
// returns the client interface for using it. Background mining is initiated so created transactions
// get mined into blocks.
func CreateWalletClient(t *testing.T) WalletClient {
	return CreateWalletClientWithWalletDir(t, t.TempDir())
}

// GetBalance is a convenience method for tests that assumes you want the primary
// address and that errors should fail the test.
func GetBalance(t *testing.T, wc WalletClient) *wallet.GetBalanceResponse {
	balance, err := wc.GetBalance(0)
	require.NoError(t, err)
	return balance
}

// TestBackgroundMineBlocks starts a background go routine to mine blocks in a monerod instance
// that is in regtest mode. If there is an existing go routine that is already mining from
// a previous call, no new go routine is created.
func TestBackgroundMineBlocks(t *testing.T) {
	var wg sync.WaitGroup
	wg.Add(1)
	ctx, cancelFunc := context.WithCancel(context.Background())
	t.Cleanup(func() {
		cancelFunc()
		wg.Wait()
	})
	// Lower the sleep duration used by WaitForBlock
	blockSleepDuration = backgroundMineInterval / 3
	go func() {
		defer wg.Done()
		if !mineMu.TryLock() {
			return // If there are multiple clients in a test, only let one of them mine.
		}
		defer mineMu.Unlock()
		for {
			select {
			case <-ctx.Done():
				return
			case <-time.After(backgroundMineInterval):
				// not cancelled, mine another block below
			}

			daemonCli := monerorpc.New(MonerodRegtestEndpoint, nil).Daemon
			resp, err := daemonCli.GenerateBlocks(&daemon.GenerateBlocksRequest{
				AmountOfBlocks: 1,
				WalletAddress:  blockRewardAddress,
			})
			if err != nil && err.Error() == errBlockNotAccepted {
				// This probably happens when something else is simultaneously generating
				// blocks, not an error that matters unless it is happening frequently.
				t.Logf("Background mining had non-accepted block")
				continue
			}
			require.NoError(t, err)
			if false { // change to true if debugging and you want to see when new blocks are generated
				t.Logf("Block generated height=%d", resp.Height)
			}
		}
	}()
}

// MineMinXMRBalance enables mining for the passed wc wallet until it has an unlocked balance greater
// than or equal to minBalance.
func MineMinXMRBalance(t *testing.T, wc WalletClient, minBalance *coins.PiconeroAmount) {
	daemonCli := monerorpc.New(MonerodRegtestEndpoint, nil).Daemon
	addr, err := wc.GetAddress(0)
	require.NoError(t, err)
	t.Log("mining to address:", addr.Address)

	minBalU64, err := minBalance.Uint64()
	require.NoError(t, err)

	for {
		balance, err := wc.GetBalance(0)
		require.NoError(t, err)
		if balance.UnlockedBalance > minBalU64 {
			break
		}

		_, err = daemonCli.GenerateBlocks(&daemon.GenerateBlocksRequest{
			AmountOfBlocks: 32,
			WalletAddress:  addr.Address,
		})
		if err != nil && err.Error() == errBlockNotAccepted {
			continue
		}
		require.NoError(t, err)
	}
}
