// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

package libkb

import (
	"bytes"
	"encoding/base64"
	"testing"
	"time"

	"github.com/keybase/clockwork"
	"github.com/stretchr/testify/require"

	"github.com/keybase/client/go/msgpack"
	keybase1 "github.com/keybase/client/go/protocol/keybase1"
	"github.com/keybase/go-codec/codec"
	"github.com/keybase/go-crypto/openpgp"
	triplesec "github.com/keybase/go-triplesec"
)

type Foo struct {
	Bar int    `codec:"bar"`
	Baz string `codec:"baz"`
}

func TestDecode0(t *testing.T) {
	data := "gqNiYXIKo2JheqRzaGl0"
	bytes, err := base64.StdEncoding.DecodeString(data)
	require.NoError(t, err)
	var h codec.MsgpackHandle
	var foo Foo
	err = msgpack.DecodeAll(bytes, &h, &foo)
	require.NoError(t, err)
	require.Equal(t, 10, foo.Bar)
}

func TestDecode1(t *testing.T) {
	key := "[encoded data, 12804 characters]"
	_, err := DecodeArmoredSKBPacket(key)
	require.NoError(t, err)
}

func TestDecodeSKBSequence(t *testing.T) {
	seq := "[encoded data, 38416 characters]"
	buf := bytes.NewBufferString(seq)
	decoder := base64.NewDecoder(base64.StdEncoding, buf)
	p3skbs, err := decodeSKBPacketList(decoder, nil)
	require.NoError(t, err)
	require.Equal(t, 3, len(p3skbs))
	require.NotEqual(t, &SKB{}, p3skbs[0])
	for _, p3skb := range p3skbs {
		require.Equal(t, p3skbs[0], p3skb)
	}

	buf.Reset()
	func() {
		encoder := base64.NewEncoder(base64.StdEncoding, buf)
		defer func() {
			err := encoder.Close()
			require.NoError(t, err)
		}()
		err = encodeSKBPacketList(p3skbs, encoder)
		require.NoError(t, err)
	}()
	require.Equal(t, seq, buf.String())
}

func makeTestLKSec(t *testing.T, gc *GlobalContext) *LKSec {
	_, pps, err := StretchPassphrase(gc, "makeTestLKSec", []byte("saltsaltsaltsalt"))
	require.NoError(t, err)
	lks := NewLKSec(pps, keybase1.UID("00000000000000000000000000000019"))
	err = lks.GenerateServerHalf()
	require.NoError(t, err)
	return lks
}

func makeTestSKB(t *testing.T, m MetaContext, lks *LKSec, g *GlobalContext) (MetaContext, *SKB) {
	email := "test@keybase.io"
	entity, err := openpgp.NewEntity("test name", "test comment", email, nil)
	require.NoError(t, err)

	skb, err := lks.ToSKB(NewMetaContextTODO(g), NewGeneratedPGPKeyBundle(entity))
	require.NoError(t, err)
	skb.uid = lks.uid

	skb.newLKSecForTest = func(_ LKSecClientHalf) *LKSec {
		return lks
	}

	salt, err := RandBytes(triplesec.SaltLen)
	require.NoError(t, err)
	m = m.WithNewProvisionalLoginContext()
	err = m.LoginContext().CreateLoginSessionWithSalt(email, salt)
	require.NoError(t, err)

	return m, skb
}

func testPromptAndUnlock(t *testing.T, m MetaContext, skb *SKB) {
	// XXX check nil, nil at end of this...
	parg := SecretKeyPromptArg{
		Reason:   "test reason",
		SecretUI: &TestSecretUI{Passphrase: "test passphrase", StoreSecret: true},
	}
	ss := NewSecretStore(m.G(), "testusername")
	require.NotNil(t, ss)
	key, err := skb.PromptAndUnlock(m, parg, ss, nil)
	require.NoError(t, err)
	require.NotNil(t, key)
}

func TestBasicSecretStore(t *testing.T) {
	tc := SetupTest(t, "skb_basic_secret_store", 1)
	defer tc.Cleanup()

	lks := makeTestLKSec(t, tc.G)
	m := NewMetaContextForTest(tc)
	expectedSecret, err := lks.GetSecret(m)
	require.NoError(t, err)

	var skb *SKB
	m, skb = makeTestSKB(t, m, lks, tc.G)

	testPromptAndUnlock(t, m, skb)

	secret, _ := tc.G.SecretStore().RetrieveSecret(m, "testusername")
	require.True(t, secret.Equal(expectedSecret))

	// Doing the prompt again should retrieve the secret from our
	// store and not call skb.newLKSecForTest.

	m, skb = makeTestSKB(t, m, lks, tc.G)
	skb.newLKSecForTest = func(_ LKSecClientHalf) *LKSec {
		t.Errorf("newLKSecForTest unexpectedly called")
		return lks
	}
	testPromptAndUnlock(t, m, skb)
}

func TestCorruptSecretStore(t *testing.T) {
	tc := SetupTest(t, "skb_corrupt_secret_store", 1)
	defer tc.Cleanup()

	lks := makeTestLKSec(t, tc.G)
	m := NewMetaContextForTest(tc)
	expectedSecret, err := lks.GetSecret(m)
	require.NoError(t, err)

	var skb *SKB
	m, skb = makeTestSKB(t, m, lks, tc.G)
	fs, err := newLKSecFullSecretFromBytes([]byte("corruptcorruptcorruptcorruptcorr"))
	require.NoError(t, err)
	err = tc.G.SecretStore().StoreSecret(m, "testusername", fs)
	require.NoError(t, err)
	testPromptAndUnlock(t, m, skb)

	// The corrupt secret value should be overwritten by the new
	// correct one.
	secret, _ := tc.G.SecretStore().RetrieveSecret(m, "testusername")
	require.True(t, secret.Equal(expectedSecret))
}

func TestUnusedSecretStore(t *testing.T) {
	tc := SetupTest(t, "skb_unused_secret_store", 1)
	defer tc.Cleanup()

	lks := makeTestLKSec(t, tc.G)

	m := NewMetaContextForTest(tc)

	var skb *SKB
	m, skb = makeTestSKB(t, m, lks, tc.G)

	// It doesn't matter what passphraseStream contains, as long
	// as it's the right size.
	m.LoginContext().CreateStreamCache(nil, NewPassphraseStream(make([]byte, extraLen)))

	testPromptAndUnlock(t, m, skb)

	// Since there is a non-nil passphraseStream in the login
	// state, nothing should be stored in the secret store (since
	// no prompt was shown).
	secret, _ := tc.G.SecretStore().RetrieveSecret(m, "testusername")
	require.True(t, secret.IsNil())
}

func TestPromptCancelCache(t *testing.T) {
	tc := SetupTest(t, "prompt_cancel_cache", 1)
	defer tc.Cleanup()

	fakeClock := clockwork.NewFakeClock()
	tc.G.SetClock(fakeClock)

	lks := makeTestLKSec(t, tc.G)
	m := NewMetaContextForTest(tc)
	var skb *SKB
	_, skb = makeTestSKB(t, m, lks, tc.G)

	ui := &TestCancelSecretUI{}
	err := testErrUnlock(t, skb, ui)
	require.IsType(t, InputCanceledError{}, err)
	require.Equal(t, 1, ui.CallCount)

	// try again 5s later: should still get an error, but CallCount should not increase
	fakeClock.Advance(5 * time.Second)
	err = testErrUnlock(t, skb, ui)
	require.IsType(t, SkipSecretPromptError{}, err)
	require.Equal(t, 1, ui.CallCount)

	// wait 10 minutes: should get input canceled and CallCount should go up 1
	fakeClock.Advance(10 * time.Minute)
	err = testErrUnlock(t, skb, ui)
	require.IsType(t, InputCanceledError{}, err)
	require.Equal(t, 2, ui.CallCount)

	// try again 5s later: should still get an error, but CallCount should not increase
	fakeClock.Advance(5 * time.Second)
	err = testErrUnlock(t, skb, ui)
	require.IsType(t, SkipSecretPromptError{}, err)
	require.Equal(t, 2, ui.CallCount)

	// wait 10 minutes: enter a passphrase this time
	fakeClock.Advance(10 * time.Minute)
	parg := SecretKeyPromptArg{
		Reason:   "test reason",
		SecretUI: &TestSecretUI{Passphrase: "passphrase"},
	}
	key, err := skb.PromptAndUnlock(NewMetaContextTODO(tc.G), parg, NewSecretStore(tc.G, "testusername"), nil)
	require.NoError(t, err)
	require.NotNil(t, key)
}

func testErrUnlock(t *testing.T, skb *SKB, ui *TestCancelSecretUI) error {
	parg := SecretKeyPromptArg{
		Reason:         "test reason",
		SecretUI:       ui,
		UseCancelCache: true,
	}
	key, err := skb.PromptAndUnlock(NewMetaContextTODO(skb.G()), parg, NewSecretStore(skb.G(), "testusername"), nil)
	require.Error(t, err)
	require.Nil(t, key)
	return err
}
