package service

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/keys-pub/keys"
	"github.com/keys-pub/keys/http"
	"github.com/keys-pub/keys/saltpack"
	"github.com/stretchr/testify/require"
)

func TestSignVerify(t *testing.T) {
	var err error
	env := newTestEnv(t)
	aliceService, aliceCloseFn := newTestService(t, env)
	defer aliceCloseFn()
	testAuthSetup(t, aliceService)
	testImportKey(t, aliceService, alice)

	// Sign with kid (alice)
	message := "I'm alice"
	signResp, err := aliceService.Sign(context.TODO(), &SignRequest{Data: []byte(message), Signer: alice.ID().String()})
	require.NoError(t, err)
	require.NotEmpty(t, signResp.Data)
	require.Equal(t, alice.ID().String(), signResp.KID)

	verifyResp, err := aliceService.Verify(context.TODO(), &VerifyRequest{Data: signResp.Data})
	require.NoError(t, err)
	require.Equal(t, message, string(verifyResp.Data))
	require.Equal(t, alice.ID().String(), verifyResp.Signer.ID)

	testUserSetupGithub(t, env, aliceService, alice, "alice")

	// Sign with alice@github
	signResp, err = aliceService.Sign(context.TODO(), &SignRequest{Data: []byte(message), Signer: "alice@github"})
	require.NoError(t, err)
	require.NotEmpty(t, signResp.Data)
	require.Equal(t, alice.ID().String(), signResp.KID)

	verifyResp, err = aliceService.Verify(context.TODO(), &VerifyRequest{Data: signResp.Data})
	require.NoError(t, err)
	require.Equal(t, message, string(verifyResp.Data))
	require.NotNil(t, verifyResp.Signer)
	require.Equal(t, alice.ID().String(), verifyResp.Signer.ID)
	require.NotNil(t, verifyResp.Signer.User)
	require.Equal(t, "alice", verifyResp.Signer.User.Name)

	// Bob
	bobService, bobCloseFn := newTestService(t, env)
	defer bobCloseFn()
	testAuthSetup(t, bobService)

	// Verify at bob
	verifyResp, err = bobService.Verify(context.TODO(), &VerifyRequest{Data: signResp.Data})
	require.NoError(t, err)
	require.Equal(t, message, string(verifyResp.Data))
	require.NotNil(t, verifyResp.Signer)
	require.Equal(t, alice.ID().String(), verifyResp.Signer.ID)
	require.Nil(t, verifyResp.Signer.User)

	// View key (do not import)
	_, err = bobService.Key(context.TODO(), &KeyRequest{Key: alice.ID().String(), Update: true})
	require.NoError(t, err)

	// Re-verify
	verifyResp, err = bobService.Verify(context.TODO(), &VerifyRequest{Data: signResp.Data})
	require.NoError(t, err)
	require.Equal(t, message, string(verifyResp.Data))
	require.NotNil(t, verifyResp.Signer)
	require.Equal(t, alice.ID().String(), verifyResp.Signer.ID)
	require.Nil(t, verifyResp.Signer.User)

	// Sign (not found)
	key := keys.GenerateEdX25519Key()
	_, err = aliceService.Sign(context.TODO(), &SignRequest{Data: []byte(message), Signer: key.ID().String()})
	require.EqualError(t, err, fmt.Sprintf("%s not found", key.ID()))
}

func TestSignStream(t *testing.T) {
	env := newTestEnv(t)
	service, closeFn := newTestService(t, env)
	defer closeFn()
	testAuthSetup(t, service)
	testImportKey(t, service, alice)

	testSignStream(t, env, service, bytes.Repeat([]byte{0x31}, 5), alice.ID().String())
	testSignStream(t, env, service, bytes.Repeat([]byte{0x31}, (1024*1024)+5), alice.ID().String())
	// TODO: Test timeout if data stops streaming
}

func testSignStream(t *testing.T, env *testEnv, service *service, plaintext []byte, signer string) {
	ctx, cancel := context.WithCancel(context.TODO())
	defer cancel()
	var clientOut bytes.Buffer
	cl, clientCloseFn := newTestRPCClient(t, service, env, "", &clientOut)
	defer clientCloseFn()

	streamClient, streamErr := cl.RPCClient().SignStream(ctx)
	require.NoError(t, streamErr)

	chunkSize := 1024 * 1024
	go func() {
		done := false
		err := streamClient.Send(&SignInput{
			Signer:  signer,
			Armored: true,
		})
		require.NoError(t, err)
		for chunk := 0; true; chunk++ {
			s, e := (chunk * chunkSize), ((chunk + 1) * chunkSize)
			if e > len(plaintext) {
				e = len(plaintext)
				done = true
			}
			logger.Debugf("(Test) Send chunk %d", len(plaintext[s:e]))
			err := streamClient.Send(&SignInput{
				Data: plaintext[s:e],
			})
			require.NoError(t, err)
			if done {
				logger.Debugf("(Test) Send done")
				break
			}
		}
		logger.Debugf("(Test) Close send")
		closeErr2 := streamClient.CloseSend()
		require.NoError(t, closeErr2)
	}()

	var buf bytes.Buffer
	for {
		resp, recvErr := streamClient.Recv()
		if recvErr != nil {
			if recvErr == io.EOF {
				logger.Debugf("(Test) Recv EOF, done")
				break
			}
			require.NoError(t, recvErr)
		}
		logger.Infof("(Test) Recv %d", len(resp.Data))
		_, writeErr := buf.Write(resp.Data)
		require.NoError(t, writeErr)
	}

	// Verify (from Saltpack)
	data := buf.Bytes()
	out, sout, err := saltpack.Verify(data)
	require.NoError(t, err)
	if signer != "" {
		require.Equal(t, sout.String(), signer)
	}
	require.Equal(t, plaintext, out)

	// Verify stream
	outClient, streamErr := cl.RPCClient().VerifyStream(ctx)
	require.NoError(t, streamErr)

	go func() {
		done := false
		for chunk := 0; ; chunk++ {
			s, e := (chunk * chunkSize), ((chunk + 1) * chunkSize)
			if e > len(data) {
				e = len(data)
				done = true
			}
			err := outClient.Send(&VerifyInput{
				Data: data[s:e],
			})
			require.NoError(t, err)
			if done {
				break
			}
		}
		closeErr2 := outClient.CloseSend()
		require.NoError(t, closeErr2)
	}()

	var bufOut bytes.Buffer
	for {
		resp, recvErr := outClient.Recv()
		require.NoError(t, recvErr)
		if len(resp.Data) == 0 {
			break
		}
		_, writeErr := bufOut.Write(resp.Data)
		require.NoError(t, writeErr)
	}

	require.Equal(t, plaintext, bufOut.Bytes())
}

func TestSignVerifyAttachedFile(t *testing.T) {
	env := newTestEnv(t)

	aliceService, aliceCloseFn := newTestService(t, env)
	defer aliceCloseFn()
	testAuthSetup(t, aliceService)
	testImportKey(t, aliceService, alice)

	bobService, bobCloseFn := newTestService(t, env)
	defer bobCloseFn()
	testAuthSetup(t, bobService)
	testImportKey(t, bobService, bob)

	b := []byte("test message")
	inPath := keys.RandTempPath()
	outPath := inPath + ".signed"
	verifiedPath := inPath + ".ver"

	defer os.Remove(inPath)
	defer os.Remove(outPath)
	defer os.Remove(verifiedPath)

	writeErr := ioutil.WriteFile(inPath, b, 0644)
	require.NoError(t, writeErr)

	var aliceOut bytes.Buffer
	aliceClient, aliceClientCloseFn := newTestRPCClient(t, aliceService, env, "", &aliceOut)
	defer aliceClientCloseFn()

	err := signFile(aliceClient, alice.ID().String(), true, false, inPath, outPath)
	require.NoError(t, err)
	require.Equal(t, fmt.Sprintf("out: %s.signed\n", inPath), aliceOut.String())

	var bobOut bytes.Buffer
	bobClient, bobClientCloseFn := newTestRPCClient(t, bobService, env, "", &bobOut)
	defer bobClientCloseFn()

	_, err = verifyFile(bobClient, outPath, verifiedPath, alice.ID().String())
	require.NoError(t, err)

	bout, err := ioutil.ReadFile(verifiedPath)
	require.NoError(t, err)
	require.Equal(t, b, bout)
	os.Remove(verifiedPath)

	// Test nextPath
	// out, err := verifyFile(bobClient, true, outPath, "", alice.ID().String())
	// require.NoError(t, err)
	// require.Equal(t, inPath+"-1", out)
	// os.Remove(out)
}

func TestVerifyUnverified(t *testing.T) {
	// SetLogger(NewLogger(DebugLevel))
	// keys.SetLogger(NewLogger(DebugLevel))
	env := newTestEnv(t)

	aliceService, aliceCloseFn := newTestService(t, env)
	defer aliceCloseFn()
	testAuthSetup(t, aliceService)
	testImportKey(t, aliceService, alice)
	testUserSetupGithub(t, env, aliceService, alice, "alice")

	bobService, bobCloseFn := newTestService(t, env)
	defer bobCloseFn()
	testAuthSetup(t, bobService)
	testImportKey(t, bobService, bob)
	tub := testUserSetupGithub(t, env, bobService, bob, "bob")

	testPull(t, aliceService, bob.ID())

	env.clock.Add(time.Hour * 24)

	// Set 500 error for bob@github
	env.client.SetProxy(tub.URL, func(ctx context.Context, req *http.Request, headers []http.Header) http.ProxyResponse {
		return http.ProxyResponse{Err: http.Error{StatusCode: 500}}
	})

	// Sign (bob)
	signResp, err := bobService.Sign(context.TODO(), &SignRequest{
		Signer: bob.ID().String(),
		Data:   []byte("test"),
	})
	require.NoError(t, err)

	// Verify (bob, error)
	_, err = aliceService.Verify(context.TODO(), &VerifyRequest{
		Data: signResp.Data,
	})
	require.EqualError(t, err, "user bob@github has failed status connection-fail")
}
