// Copyright 2019 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.

package main

import (
	"context"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"testing"
	"time"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/rpc"
	mockRPC "github.com/ethereum/go-ethereum/swarm/storage/mock/rpc"
)

// TestHTTP_InMemory tests in-memory global store that exposes
// HTTP server.
func TestHTTP_InMemory(t *testing.T) {
	testHTTP(t, true)
}

// TestHTTP_Database tests global store with persisted database
// that exposes HTTP server.
func TestHTTP_Database(t *testing.T) {
	dir, err := ioutil.TempDir("", "swarm-global-store-")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	// create a fresh global store
	testHTTP(t, true, "--dir", dir)

	// check if data saved by the previous global store instance
	testHTTP(t, false, "--dir", dir)
}

// testWebsocket starts global store binary with HTTP server
// and validates that it can store and retrieve data.
// If put is false, no data will be stored, only retrieved,
// giving the possibility to check if data is present in the
// storage directory.
func testHTTP(t *testing.T, put bool, args ...string) {
	addr := findFreeTCPAddress(t)
	testCmd := runGlobalStore(t, append([]string{"http", "--addr", addr}, args...)...)
	defer testCmd.Kill()

	client, err := rpc.DialHTTP("http://" + addr)
	if err != nil {
		t.Fatal(err)
	}

	// wait until global store process is started as
	// rpc.DialHTTP is actually not connecting
	waitHTTPEndpoint(t, addr)

	store := mockRPC.NewGlobalStore(client)
	defer store.Close()

	node := store.NewNodeStore(common.HexToAddress("123abc"))

	wantKey := "key"
	wantValue := "value"

	if put {
		err = node.Put([]byte(wantKey), []byte(wantValue))
		if err != nil {
			t.Fatal(err)
		}
	}

	gotValue, err := node.Get([]byte(wantKey))
	if err != nil {
		t.Fatal(err)
	}

	if string(gotValue) != wantValue {
		t.Errorf("got value %s for key %s, want %s", string(gotValue), wantKey, wantValue)
	}
}

// TestWebsocket_InMemory tests in-memory global store that exposes
// WebSocket server.
func TestWebsocket_InMemory(t *testing.T) {
	testWebsocket(t, true)
}

// TestWebsocket_Database tests global store with persisted database
// that exposes HTTP server.
func TestWebsocket_Database(t *testing.T) {
	dir, err := ioutil.TempDir("", "swarm-global-store-")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	// create a fresh global store
	testWebsocket(t, true, "--dir", dir)

	// check if data saved by the previous global store instance
	testWebsocket(t, false, "--dir", dir)
}

// testWebsocket starts global store binary with WebSocket server
// and validates that it can store and retrieve data.
// If put is false, no data will be stored, only retrieved,
// giving the possibility to check if data is present in the
// storage directory.
func testWebsocket(t *testing.T, put bool, args ...string) {
	addr := findFreeTCPAddress(t)
	testCmd := runGlobalStore(t, append([]string{"ws", "--addr", addr}, args...)...)
	defer testCmd.Kill()

	client := websocketClient(t, addr)

	store := mockRPC.NewGlobalStore(client)
	defer store.Close()

	node := store.NewNodeStore(common.HexToAddress("123abc"))

	wantKey := "key"
	wantValue := "value"

	if put {
		err := node.Put([]byte(wantKey), []byte(wantValue))
		if err != nil {
			t.Fatal(err)
		}
	}

	gotValue, err := node.Get([]byte(wantKey))
	if err != nil {
		t.Fatal(err)
	}

	if string(gotValue) != wantValue {
		t.Errorf("got value %s for key %s, want %s", string(gotValue), wantKey, wantValue)
	}
}

// findFreeTCPAddress returns a local address (IP:Port) to which
// global store can listen on.
func findFreeTCPAddress(t *testing.T) (addr string) {
	t.Helper()

	listener, err := net.Listen("tcp", "")
	if err != nil {
		t.Fatal(err)
	}
	defer listener.Close()

	return listener.Addr().String()
}

// websocketClient waits until global store process is started
// and returns rpc client.
func websocketClient(t *testing.T, addr string) (client *rpc.Client) {
	t.Helper()

	var err error
	for i := 0; i < 1000; i++ {
		client, err = rpc.DialWebsocket(context.Background(), "ws://"+addr, "")
		if err == nil {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if err != nil {
		t.Fatal(err)
	}
	return client
}

// waitHTTPEndpoint retries http requests to a provided
// address until the connection is established.
func waitHTTPEndpoint(t *testing.T, addr string) {
	t.Helper()

	var err error
	for i := 0; i < 1000; i++ {
		_, err = http.Get("http://" + addr)
		if err == nil {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if err != nil {
		t.Fatal(err)
	}
}
