package service

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base32"
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"net/http/httptest"
	"os"
	"strings"
	"testing"

	"github.com/keys-pub/keys"
	"github.com/keys-pub/keys-ext/http/server"
	"github.com/keys-pub/keys/api"
	"github.com/keys-pub/keys/dstore"
	"github.com/keys-pub/keys/http"
	"github.com/keys-pub/keys/tsutil"
	"github.com/keys-pub/keys/users"
	"github.com/stretchr/testify/require"
)

func newEnv(t *testing.T, appName string, serverURL string) (*Env, CloseFn) {
	if appName == "" {
		appName = "KeysTest-" + randName()
	}
	env, err := NewEnv(appName)
	require.NoError(t, err)
	env.Set(serverCfgKey, serverURL)

	closeFn := func() {
		removeErr := os.RemoveAll(env.AppDir())
		require.NoError(t, removeErr)
	}
	return env, closeFn
}

func randName() string {
	buf := make([]byte, 8)
	if _, err := rand.Read(buf); err != nil {
		panic(err)
	}
	return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(buf)
}

func writeTestFile(t *testing.T) string {
	inPath := keys.RandTempPath() + ".txt"
	writeErr := ioutil.WriteFile(inPath, []byte("test message"), 0644)
	require.NoError(t, writeErr)
	return inPath
}

func testFire(t *testing.T, clock tsutil.Clock) server.Fire {
	fi := dstore.NewMem()
	fi.SetClock(clock)
	return fi
}

func testSeed(b byte) *[32]byte {
	return keys.Bytes32(bytes.Repeat([]byte{b}, 32))
}

type testEnv struct {
	clock  tsutil.Clock
	fi     server.Fire
	client http.Client
	users  *users.Users
}

func newTestEnv(t *testing.T) *testEnv {
	clock := tsutil.NewTestClock()
	fi := testFire(t, clock)
	client := http.NewClient()
	usrs := users.New(fi, keys.NewSigchains(fi), users.Client(client), users.Clock(clock))
	return &testEnv{
		clock:  clock,
		fi:     fi,
		client: client,
		users:  usrs,
	}
}

func newTestService(t *testing.T, tenv *testEnv) (*service, CloseFn) {
	serverEnv := newTestServerEnv(t, tenv)
	appName := "KeysTest-" + randName()

	env, closeFn := newEnv(t, appName, serverEnv.url)
	auth := newAuth(env)

	svc, err := newService(env, Build{Version: "1.2.3", Commit: "deadbeef"}, auth, tenv.client, tenv.clock)
	require.NoError(t, err)

	err = svc.Open()
	require.NoError(t, err)

	closeServiceFn := func() {
		serverEnv.closeFn()
		svc.Close()
		closeFn()
	}

	return svc, closeServiceFn
}

var authPassword = "testpassword"

func testAuthSetup(t *testing.T, service *service) {
	_, err := service.AuthSetup(context.TODO(), &AuthSetupRequest{
		Secret: authPassword,
		Type:   PasswordAuth,
	})
	require.NoError(t, err)
	_, err = service.AuthUnlock(context.TODO(), &AuthUnlockRequest{
		Secret: authPassword,
		Type:   PasswordAuth,
		Client: "test",
	})
	require.NoError(t, err)
}

func testAuthLock(t *testing.T, service *service) {
	_, err := service.AuthLock(context.TODO(), &AuthLockRequest{})
	require.NoError(t, err)
}

func testAuthUnlock(t *testing.T, service *service) {
	_, err := service.AuthUnlock(context.TODO(), &AuthUnlockRequest{
		Secret: authPassword,
		Type:   PasswordAuth,
		Client: "test",
	})
	require.NoError(t, err)
}

// func testAuthVault(t *testing.T, service *service, key *keys.EdX25519Key) {
// 	_, err := service.AuthVault(context.TODO(), &AuthVaultRequest{
// 		Key: encoding.MustEncode(key.Seed()[:], encoding.BIP39),
// 	})
// 	require.NoError(t, err)
// }

func testImportKey(t *testing.T, service *service, key *keys.EdX25519Key) {
	encoded, err := api.EncodeKey(api.NewKey(key), authPassword)
	require.NoError(t, err)
	_, err = service.KeyImport(context.TODO(), &KeyImportRequest{
		In:       []byte(encoded),
		Password: authPassword,
	})
	require.NoError(t, err)
}

func testImportID(t *testing.T, service *service, kid keys.ID) {
	_, err := service.KeyImport(context.TODO(), &KeyImportRequest{
		In: []byte(kid.String()),
	})
	require.NoError(t, err)
}

type testUser struct {
	URL      string
	Response string
}

func userSetupGithub(env *testEnv, service *service, key *keys.EdX25519Key, username string) (*testUser, error) {
	serviceName := "github"
	resp, err := service.UserSign(context.TODO(), &UserSignRequest{
		KID:     key.ID().String(),
		Service: serviceName,
		Name:    username,
	})
	if err != nil {
		return nil, err
	}

	id := hex.EncodeToString(sha256.New().Sum([]byte(serviceName + "/" + username))[:8])
	url := fmt.Sprintf("https://gist.github.com/%s/%s", username, id)
	api := "https://api.github.com/gists/" + id
	body := []byte(githubMock(username, id, resp.Message))
	env.client.SetProxy(api, func(ctx context.Context, req *http.Request, headers []http.Header) http.ProxyResponse {
		return http.ProxyResponse{Body: body}
	})

	_, err = service.UserAdd(context.TODO(), &UserAddRequest{
		KID:     key.ID().String(),
		Service: serviceName,
		Name:    username,
		URL:     url,
	})
	return &testUser{URL: api, Response: string(body)}, err
}

func testUserSetupGithub(t *testing.T, env *testEnv, service *service, key *keys.EdX25519Key, username string) *testUser {
	tu, err := userSetupGithub(env, service, key, username)
	require.NoError(t, err)
	return tu
}

func githubMock(name string, id string, msg string) string {
	msg = strings.ReplaceAll(msg, "\n", "")
	return `{
		"id": "` + id + `",
		"files": {
			"gistfile1.txt": {
				"content": "` + msg + `"
			}		  
		},
		"owner": {
			"login": "` + name + `"
		}
	  }`
}

func redditMock(author string, msg string, subreddit string) string {
	msg = strings.ReplaceAll(msg, "\n", " ")
	return `[{   
		"kind": "Listing",
		"data": {
			"children": [
				{
					"kind": "t3",
					"data": {
						"author": "` + author + `",
						"selftext": "` + msg + `",
						"subreddit": "` + subreddit + `"
					}
				}
			]
		}
    }]`
}

// func mockRedditURL(url string) string {
// 	return url + ".json"
// }

// func testRemoveKey(t *testing.T, service *service, key *keys.EdX25519Key) {
// 	_, err := service.KeyRemove(context.TODO(), &KeyRemoveRequest{
// 		KID: key.ID().String(),
// 	})
// 	require.NoError(t, err)
// }

func testPush(t *testing.T, service *service, key *keys.EdX25519Key) {
	_, err := service.Push(context.TODO(), &PushRequest{
		Key: key.ID().String(),
	})
	require.NoError(t, err)
}

func testPull(t *testing.T, service *service, kid keys.ID) {
	_, err := service.Pull(context.TODO(), &PullRequest{
		Key: kid.String(),
	})
	require.NoError(t, err)
}

// func testUnlock(t *testing.T, service *service) {
// 	_, err := service.AuthUnlock(context.TODO(), &AuthUnlockRequest{
// 		Password: keys.RandPassphrase(12),
// 		Type: PasswordAuth,
//      Client: "test",
// 	})
// 	require.NoError(t, err)
// }

type serverEnv struct {
	url     string
	closeFn func()
}

func newTestServerEnv(t *testing.T, env *testEnv) *serverEnv {
	rds := server.NewRedisTest(env.clock)
	srv := server.New(env.fi, rds, env.client, env.clock, server.NewLogger(server.NoLevel))
	srv.SetClock(env.clock)
	tasks := server.NewTestTasks(srv)
	srv.SetTasks(tasks)
	srv.SetInternalAuth("testtoken")
	_ = srv.SetInternalKey("6a169a699f7683c04d127504a12ace3b326e8b56a61a9b315cf6b42e20d6a44a")
	handler := server.NewHandler(srv)
	testServer := httptest.NewServer(handler)
	srv.URL = testServer.URL

	closeFn := func() {
		testServer.Close()
	}
	return &serverEnv{
		url:     srv.URL,
		closeFn: closeFn,
	}
}

func TestRuntimeStatus(t *testing.T) {
	env := newTestEnv(t)
	service, closeFn := newTestService(t, env)
	defer closeFn()

	resp, err := service.RuntimeStatus(context.TODO(), &RuntimeStatusRequest{})
	require.NoError(t, err)
	require.Equal(t, "1.2.3", resp.Version)
}

func TestCheckKeys(t *testing.T) {
	env := newTestEnv(t)
	service, closeFn := newTestService(t, env)
	defer closeFn()

	testAuthSetup(t, service)

	testImportKey(t, service, alice)
	testUserSetupGithub(t, env, service, alice, "alice")
	testPush(t, service, alice)

	err := service.checkKeys(context.TODO())
	require.NoError(t, err)
}

func TestServiceCheck(t *testing.T) {
	// SetLogger(NewLogger(DebugLevel))
	// vault.SetLogger(NewLogger(DebugLevel))
	env := newTestEnv(t)
	service, closeFn := newTestService(t, env)
	defer closeFn()

	testAuthSetup(t, service)
	require.True(t, service.checking)

	testAuthLock(t, service)
	require.False(t, service.checking)

	testAuthUnlock(t, service)
	require.True(t, service.checking)

	testAuthLock(t, service)
	require.False(t, service.checking)
}
