package jail

import (
	"encoding/json"
	"testing"

	"github.com/robertkrimen/otto"
	"github.com/status-im/status-go/geth/rpc"
	"github.com/stretchr/testify/suite"
)

type testRPCClientProvider struct {
	rpcClient *rpc.Client
}

func (p testRPCClientProvider) RPCClient() *rpc.Client {
	return p.rpcClient
}

func TestJailTestSuite(t *testing.T) {
	suite.Run(t, new(JailTestSuite))
}

type JailTestSuite struct {
	suite.Suite
	Jail *Jail
}

func (s *JailTestSuite) SetupTest() {
	s.Jail = New(nil)
}

func (s *JailTestSuite) TestJailCreateCell() {
	cell, err := s.Jail.CreateCell("cell1")
	s.NoError(err)
	s.NotNil(cell)
	// creating another cell with the same id fails
	_, err = s.Jail.CreateCell("cell1")
	s.EqualError(err, "cell with id 'cell1' already exists")

	// create more cells
	_, err = s.Jail.CreateCell("cell2")
	s.NoError(err)
	_, err = s.Jail.CreateCell("cell3")
	s.NoError(err)
	s.Len(s.Jail.cells, 3)
}

func (s *JailTestSuite) TestJailGetCell() {
	// cell1 does not exist
	_, err := s.Jail.Cell("cell1")
	s.EqualError(err, "cell 'cell1' not found")

	// cell 1 exists
	_, err = s.Jail.CreateCell("cell1")
	s.NoError(err)
	cell, err := s.Jail.Cell("cell1")
	s.NoError(err)
	s.NotNil(cell)
}

func (s *JailTestSuite) TestJailInitCell() {
	// InitCell on an existing cell.
	cell, err := s.Jail.obtainCell("cell1", false)
	s.NoError(err)
	err = s.Jail.initCell(cell)
	s.NoError(err)

	// web3 should be available
	value, err := cell.Run("web3.fromAscii('ethereum')")
	s.NoError(err)
	s.Equal(`0x657468657265756d`, value.Value().String())
}

func (s *JailTestSuite) TestJailStop() {
	_, err := s.Jail.CreateCell("cell1")
	s.NoError(err)
	s.Len(s.Jail.cells, 1)

	s.Jail.Stop()

	s.Len(s.Jail.cells, 0)
}

func (s *JailTestSuite) TestJailCall() {
	cell, err := s.Jail.CreateCell("cell1")
	s.NoError(err)

	propsc := make(chan string, 1)
	argsc := make(chan string, 1)
	err = cell.Set("call", func(call otto.FunctionCall) otto.Value {
		propsc <- call.Argument(0).String()
		argsc <- call.Argument(1).String()

		return otto.UndefinedValue()
	})
	s.NoError(err)

	result := s.Jail.Call("cell1", `["prop1", "prop2"]`, `arg1`)
	s.Equal(`["prop1", "prop2"]`, <-propsc)
	s.Equal(`arg1`, <-argsc)
	s.Equal(`{"result":null}`, result)
}

func (s *JailTestSuite) TestCreateAndInitCell() {
	cell, result, err := s.Jail.createAndInitCell(
		"cell1",
		`var testCreateAndInitCell1 = true`,
		`var testCreateAndInitCell2 = true`,
		`testCreateAndInitCell2`,
	)
	s.NoError(err)
	s.NotNil(cell)
	s.Equal(`{"result":true}`, result)

	value, err := cell.Get("testCreateAndInitCell1")
	s.NoError(err)
	s.Equal(`true`, value.Value().String())

	value, err = cell.Get("testCreateAndInitCell2")
	s.NoError(err)
	s.Equal(`true`, value.Value().String())
}

func (s *JailTestSuite) TestPublicCreateAndInitCell() {
	var createAndInitTests = []struct {
		chatID      string
		input       []string
		expectation string
	}{
		{"cell1", []string{}, EmptyResponse},
		{"cell1", []string{"var a = 2", "a"}, `{"result":2}`},
		{"cell1", []string{`var a = "hello"`, "a"}, `{"result":"hello"}`},
		{"cell1", []string{`var b = "2"; var a = b * b`, "a"}, `{"result":4}`},
	}
	for _, v := range createAndInitTests {
		response := s.Jail.CreateAndInitCell(v.chatID, v.input...)
		s.Equal(v.expectation, response)
	}
}

func (s *JailTestSuite) TestNewJailResultResponseReturnsValidJson() {
	var newJailResultResponseTests = []interface{}{
		`Double quoted "success" response`,
		float64(1),
		true,
	}
	for _, input := range newJailResultResponseTests {
		v, err := otto.ToValue(input)
		s.NoError(err)

		output := newJailResultResponse(formatOttoValue(v))
		var response struct {
			Result interface{} `json:"result"`
		}
		err = json.Unmarshal([]byte(output), &response)

		s.NoError(err)
		s.Equal(input, response.Result)
	}
}

func (s *JailTestSuite) TestPublicCreateAndInitCellConsecutive() {
	response1 := s.Jail.CreateAndInitCell("cell1", `var _status_catalog = { test: true }; JSON.stringify(_status_catalog);`)
	s.Contains(response1, "test")
	cell1, err := s.Jail.Cell("cell1")
	s.NoError(err)

	// Create it again
	response2 := s.Jail.CreateAndInitCell("cell1", `var _status_catalog = { test: true, foo: 5 }; JSON.stringify(_status_catalog);`)
	s.Contains(response2, "test", "foo")
	cell2, err := s.Jail.Cell("cell1")
	s.NoError(err)

	// Second cell has to be the same object as the first one
	s.Equal(cell1, cell2)

	// Second cell must have been reinitialized
	s.NotEqual(response1, response2)
}

func (s *JailTestSuite) TestExecute() {
	// cell does not exist
	response := s.Jail.Execute("cell1", "('some string')")
	s.Equal(`{"error":"cell 'cell1' not found"}`, response)

	_, err := s.Jail.obtainCell("cell1", false)
	s.NoError(err)

	// cell exists
	response = s.Jail.Execute("cell1", `
		var obj = { test: true };
		JSON.stringify(obj);
	`)
	s.Equal(`{"test":true}`, response)
}

func (s *JailTestSuite) TestGetObjectValue() {
	cell, result, err := s.Jail.createAndInitCell(
		"cell1",
		`var testCreateAndInitCell1 = {obj: 'objValue'}`,
		`var testCreateAndInitCell2 = true`,
		`testCreateAndInitCell2`,
	)
	s.NoError(err)
	s.NotNil(cell)
	s.Equal(`{"result":true}`, result)

	testCreateAndInitCell1, err := cell.Get("testCreateAndInitCell1")
	s.NoError(err)
	s.True(testCreateAndInitCell1.Value().IsObject())
	value, err := cell.GetObjectValue(testCreateAndInitCell1.Value(), "obj")
	s.NoError(err)
	s.Equal("objValue", value.Value().String())

	value, err = cell.Get("testCreateAndInitCell2")
	s.NoError(err)
	s.Equal(`true`, value.Value().String())
}