package whisper

import (
	"context"
	"crypto/rand"
	"errors"
	"testing"
	"time"

	"github.com/ethereum/go-ethereum/accounts/keystore"
	gethcommon "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
	"github.com/status-im/status-go/e2e"
	"github.com/status-im/status-go/geth/common"
	"github.com/status-im/status-go/geth/params"
	"github.com/status-im/status-go/static"
	. "github.com/status-im/status-go/testing"
	"github.com/stretchr/testify/suite"
)

const (
	//nolint: unused, varcheck
	whisperMessage1 = `test message 1 (K1 -> K2, signed+encrypted, from us)`
	whisperMessage2 = `test message 3 (K1 -> "", signed broadcast)`
	whisperMessage3 = `test message 4 ("" -> "", anon broadcast)`
	whisperMessage4 = `test message 5 ("" -> K1, encrypted anon broadcast)`
	whisperMessage5 = `test message 6 (K2 -> K1, signed+encrypted, to us)`
)

var (
	baseStatusJSCode = string(static.MustAsset("testdata/jail/status.js"))
)

func TestWhisperJailTestSuite(t *testing.T) {
	s := new(WhisperJailTestSuite)
	s.Timeout = time.Minute * 5
	suite.Run(t, s)
}

type WhisperJailTestSuite struct {
	e2e.BackendTestSuite

	Timeout    time.Duration
	WhisperAPI *whisper.PublicWhisperAPI
	Jail       common.JailManager
}

func (s *WhisperJailTestSuite) StartTestBackend(networkID int, opts ...e2e.TestNodeOption) {
	s.BackendTestSuite.StartTestBackend(networkID, opts...)

	s.WhisperAPI = whisper.NewPublicWhisperAPI(s.WhisperService())
	s.Jail = s.Backend.JailManager()
	s.NotNil(s.Jail)
	s.Jail.BaseJS(baseStatusJSCode)
}

func (s *WhisperJailTestSuite) GetAccountKey(account struct {
	Address  string
	Password string
}) (*keystore.Key, string, error) {
	accountManager := s.Backend.AccountManager()

	_, accountKey1, err := accountManager.AddressToDecryptedAccount(account.Address, account.Password)
	if err != nil {
		return nil, "", err
	}
	accountKey1Hex := gethcommon.ToHex(crypto.FromECDSAPub(&accountKey1.PrivateKey.PublicKey))

	_, err = s.WhisperService().AddKeyPair(accountKey1.PrivateKey)
	if err != nil {
		return nil, "", err
	}

	if ok := s.WhisperAPI.HasKeyPair(context.Background(), accountKey1Hex); !ok {
		return nil, "", errors.New("KeyPair should be injected in Whisper")
	}

	return accountKey1, accountKey1Hex, nil
}

// TODO(adamb) Uncomment when issue #336 is fixed.
/*
func (s *WhisperJailTestSuite) DontTestJailWhisper() {
	s.StartTestBackend(params.StatusChainNetworkID)
	defer s.StopTestBackend()

	_, accountKey1Hex, err := s.GetAccountKey(TestConfig.Account1)
	s.NoError(err)

	_, accountKey2Hex, err := s.GetAccountKey(TestConfig.Account2)
	s.NoError(err)

	testCases := []struct {
		name      string
		code      string
		useFilter bool
	}{
		{
			"test 0: ensure correct version of Whisper is used",
			`
				var expectedVersion = '5.0';
				if (web3.version.whisper != expectedVersion) {
					throw 'unexpected shh version, expected: ' + expectedVersion + ', got: ' + web3.version.whisper;
				}
			`,
			false,
		},
		{
			"test 1: encrypted signed message from us (From != nil && To != nil)",
			`
				var identity1 = '` + accountKey1Hex + `';
				if (!shh.hasKeyPair(identity1)) {
					throw 'idenitity "` + accountKey1Hex + `" not found in whisper';
				}

				var identity2 = '` + accountKey2Hex + `';
				if (!shh.hasKeyPair(identity2)) {
					throw 'identitity "` + accountKey2Hex + `" not found in whisper';
				}

				var topic = makeTopic();
				var payload = '` + whisperMessage1 + `';

				// start watching for messages
				var filter = shh.newMessageFilter({
					sig: identity1,
					privateKeyID: identity2,
					topics: [topic]
				});

				// post message
				var message = {
					ttl: 20,
					powTarget: 0.01,
					powTime: 20,
					topic: topic,
					sig: identity1,
					pubKey: identity2,
			  		payload: web3.toHex(payload),
				};

				var sent = shh.post(message)
				if (!sent) {
					throw 'message not sent: ' + JSON.stringify(message);
				}
			`,
			true,
		},
		{
			"test 2: signed (known sender) broadcast (From != nil && To == nil)",
			`
				var identity = '` + accountKey1Hex + `';
				if (!shh.hasKeyPair(identity)) {
					throw 'idenitity "` + accountKey1Hex + `" not found in whisper';
				}

				var topic = makeTopic();
				var payload = '` + whisperMessage2 + `';

				// generate symmetric key
				var keyid = shh.newSymKey();
				if (!shh.hasSymKey(keyid)) {
					throw new Error('key not found');
				}

				// start watching for messages
				var filter = shh.newMessageFilter({
					sig: identity,
					topics: [topic],
					symKeyID: keyid
				});

				// post message
				var message = {
					ttl: 20,
					powTarget: 0.01,
					powTime: 20,
					topic: topic,
					sig: identity,
					symKeyID: keyid,
			  		payload: web3.toHex(payload),
				};

				var sent = shh.post(message)
				if (!sent) {
					throw 'message not sent: ' + JSON.stringify(message);
				}
			`,
			true,
		},
		{
			"test 3: anonymous broadcast (From == nil && To == nil)",
			`
				var topic = makeTopic();
				var payload = '` + whisperMessage3 + `';

				// generate symmetric key
				var keyid = shh.newSymKey();
				if (!shh.hasSymKey(keyid)) {
					throw new Error('key not found');
				}

				// start watching for messages
				var filter = shh.newMessageFilter({
					topics: [topic],
					symKeyID: keyid
				});

				// post message
				var message = {
					ttl: 20,
					powTarget: 0.01,
					powTime: 20,
					topic: topic,
					symKeyID: keyid,
			  		payload: web3.toHex(payload),
				};

				var sent = shh.post(message)
				if (!sent) {
					throw 'message not sent: ' + JSON.stringify(message);
				}
			`,
			true,
		},
		// @TODO(adam): quarantined as always failing. Check out TestEncryptedAnonymousMessage
		// as an equivalent test in pure Go which passes. Bug in web3?
		// {
		// 	"test 4: encrypted anonymous message (From == nil && To != nil)",
		// 	`
		// 		var identity = '` + accountKey2Hex + `';
		// 		if (!shh.hasKeyPair(identity)) {
		// 			throw 'idenitity "` + accountKey2Hex + `" not found in whisper';
		// 		}

		// 		var topic = makeTopic();
		// 		var payload = '` + whisperMessage4 + `';

		// 		// start watching for messages
		// 		var filter = shh.newMessageFilter({
		// 			privateKeyID: identity,
		// 			topics: [topic],
		// 		});

		// 		// post message
		// 		var message = {
		// 			ttl: 20,
		// 			powTarget: 0.01,
		// 			powTime: 20,
		// 			topic: topic,
		// 			pubKey: identity,
		// 	  		payload: web3.toHex(payload),
		// 		};

		// 		var sent = shh.post(message)
		// 		if (!sent) {
		// 			throw 'message not sent: ' + JSON.stringify(message);
		// 		}
		// 	`,
		// 	true,
		// },
		{
			"test 5: encrypted signed response to us (From != nil && To != nil)",
			`
				var identity1 = '` + accountKey1Hex + `';
				if (!shh.hasKeyPair(identity1)) {
					throw 'idenitity "` + accountKey1Hex + `" not found in whisper';
				}
				var identity2 = '` + accountKey2Hex + `';
				if (!shh.hasKeyPair(identity2)) {
					throw 'idenitity "` + accountKey2Hex + `" not found in whisper';
				}
				var topic = makeTopic();
				var payload = '` + whisperMessage5 + `';
				// start watching for messages
				var filter = shh.newMessageFilter({
					privateKeyID: identity1,
					sig: identity2,
					topics: [topic],
				});

				// post message
				var message = {
				  	sig: identity2,
				  	pubKey: identity1,
				  	topic: topic,
				  	payload: web3.toHex(payload),
					ttl: 20,
					powTime: 20,
					powTarget: 0.01,
				};

				var sent = shh.post(message)
				if (!sent) {
					throw 'message not sent: ' + message;
				}
			`,
			true,
		},
	}

	for _, tc := range testCases {
		s.T().Log(tc.name)

		chatID := crypto.Keccak256Hash([]byte(tc.name)).Hex()

		s.Jail.Parse(chatID, `
			var shh = web3.shh;
			// topic must be 4-byte long
			var makeTopic = function () {
				var topic = '0x';
				for (var i = 0; i < 8; i++) {
					topic += Math.floor(Math.random() * 16).toString(16);
				}
				return topic;
			};
		`)

		cell, err := s.Jail.Cell(chatID)
		s.NoError(err, "cannot get VM")

		// Setup filters and post messages.
		_, err = cell.Run(tc.code)
		s.NoError(err)

		if !tc.useFilter {
			continue
		}

		done := make(chan struct{})
		timedOut := make(chan struct{})
		go func() {
			select {
			case <-done:
			case <-time.After(s.Timeout):
				close(timedOut)
			}
		}()

	poll_loop:
		for {
			// Use polling because:
			//   (1) filterId is not assigned immediately,
			//   (2) messages propagate with some delay.
			select {
			case <-done:
				break poll_loop
			case <-timedOut:
				s.FailNow("polling for messages timed out")
			case <-time.After(time.Second):
			}

			filter, err := cell.Get("filter")
			s.NoError(err, "cannot get filter")
			filterID, err := filter.Object().Get("filterId")
			s.NoError(err, "cannot get filterId")

			// FilterID is not assigned yet.
			if filterID.IsNull() {
				continue
			}

			payload, err := cell.Get("payload")
			s.NoError(err, "cannot get payload")

			messages, err := s.WhisperAPI.GetFilterMessages(filterID.String())
			s.NoError(err)
			for _, m := range messages {
				s.Equal(payload.String(), string(m.Payload))
				close(done)
			}
		}
	}
}
*/

func (s *WhisperJailTestSuite) TestEncryptedAnonymousMessage() {
	s.StartTestBackend(params.StatusChainNetworkID)
	defer s.StopTestBackend()

	accountKey2, accountKey2Hex, err := s.GetAccountKey(TestConfig.Account2)
	s.NoError(err)

	topicSlice := make([]byte, whisper.TopicLength)
	_, err = rand.Read(topicSlice)
	s.NoError(err)

	topic := whisper.BytesToTopic(topicSlice)

	filter, err := s.WhisperAPI.NewMessageFilter(whisper.Criteria{
		PrivateKeyID: accountKey2Hex,
		Topics:       []whisper.TopicType{topic},
	})
	s.NoError(err)

	ok, err := s.WhisperAPI.Post(context.Background(), whisper.NewMessage{
		TTL:       20,
		PowTarget: 0.01,
		PowTime:   20,
		Topic:     topic,
		PublicKey: crypto.FromECDSAPub(&accountKey2.PrivateKey.PublicKey),
		Payload:   []byte(whisperMessage4),
	})
	s.NoError(err)
	s.True(ok)

	done := make(chan struct{})
	timedOut := make(chan struct{})
	go func() {
		select {
		case <-done:
		case <-time.After(s.Timeout):
			close(timedOut)
		}
	}()

	for {
		select {
		case <-done:
			return
		case <-timedOut:
			s.FailNow("polling for messages timed out")
		case <-time.After(time.Second):
		}

		messages, err := s.WhisperAPI.GetFilterMessages(filter)
		s.NoError(err)
		for _, m := range messages {
			s.Equal(whisperMessage4, string(m.Payload))
			close(done)
		}
	}
}