package engine

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"testing"
	"time"

	"github.com/keybase/client/go/libkb"
	"github.com/keybase/client/go/protocol/keybase1"
	"github.com/stretchr/testify/require"

	"github.com/keybase/go-crypto/curve25519"
	"github.com/keybase/go-crypto/openpgp"
	"github.com/keybase/go-crypto/openpgp/ecdh"
	"github.com/keybase/go-crypto/openpgp/packet"
)

func genKeyWithMaterial(uid *packet.UserId, currentTime time.Time, signingPriv *ecdsa.PrivateKey,
	encryptPriv *ecdh.PrivateKey) *openpgp.Entity {

	// We will be manipulating key creation time while keeping the eddsa
	// private key bytes same. This will yield different PGP fingerprint
	// every time (and different full hash), but same Keybase KID.

	signingPrivKey := packet.NewECDSAPrivateKey(currentTime, signingPriv)
	entity := &openpgp.Entity{
		PrimaryKey: &signingPrivKey.PublicKey,
		PrivateKey: signingPrivKey,
		Identities: make(map[string]*openpgp.Identity),
	}
	isPrimaryID := true
	entity.Identities[uid.Id] = &openpgp.Identity{
		Name:   uid.Name,
		UserId: uid,
		SelfSignature: &packet.Signature{
			CreationTime: currentTime,
			SigType:      packet.SigTypePositiveCert,
			PubKeyAlgo:   packet.PubKeyAlgoECDSA,
			Hash:         crypto.SHA512,
			IsPrimaryId:  &isPrimaryID,
			FlagsValid:   true,
			FlagSign:     true,
			FlagCertify:  true,
			IssuerKeyId:  &entity.PrimaryKey.KeyId,
		},
	}
	encryptPrivKey := packet.NewECDHPrivateKey(currentTime, encryptPriv)
	subkey := openpgp.Subkey{
		PublicKey:  &encryptPrivKey.PublicKey,
		PrivateKey: encryptPrivKey,
		Sig: &packet.Signature{
			CreationTime:              currentTime,
			SigType:                   packet.SigTypeSubkeyBinding,
			PubKeyAlgo:                packet.PubKeyAlgoECDSA,
			Hash:                      crypto.SHA512,
			FlagsValid:                true,
			FlagEncryptStorage:        true,
			FlagEncryptCommunications: true,
			IssuerKeyId:               &entity.PrimaryKey.KeyId,
		},
	}
	subkey.PrivateKey.IsSubkey = true
	subkey.PublicKey.IsSubkey = true
	entity.Subkeys = append(entity.Subkeys, subkey)
	return entity
}

func TestY2K1178(t *testing.T) {
	// Test PGP KID reuse by using the same key material for primary key.

	// PGP keys are not uniquely identified by KID. KID is only generated by
	// hashing the primary key public key material. The rest is malleable.
	// Different uids, subkeys, and signatures will yields the same key but
	// also the same PGP fingerprint. However, it is possible to generate new
	// PGP key with the same key material but different creation time. This
	// will yield a new PGP fingerprint but same Keybase KID.

	// There was an issue where we weren't properly setting `ActivePGPHash`
	// when new PGP key was being delegated after key with the same KID was
	// revoked.

	// During sigchain parsing, we would go through:
	// 1) Delegate key A with kid=X - new key, ActivePGPHash is set to A.
	// 2) Revoke kid=X. keys[X] is set to Revoked, and ActivePGPHash to "".
	// 3) Delegate key B with kid=X - there already is X, set status to
	//    Uncancelled (forgot about ActivePGPHash here).

	// This resulted in sigchain not being replayable because we were taking
	// PGP key A to verify reverse sig of PGP key B in link 3.

	signingPriv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
	require.NoError(t, err)
	// Encryption key material doesn't matter, we can keep it the same or make
	// a new one for each generated key. KID is only based on primary key
	// material.
	encPriv, err := ecdh.GenerateKey(curve25519.Cv25519(), rand.Reader)
	require.NoError(t, err)

	uid := packet.NewUserId("Keybase PGP Test", "Test Only Do Not Use", "alice@example.com")

	encode := func(entity *openpgp.Entity) []byte {
		buf, err := encodeArmoredPrivatePGP(entity)
		require.NoError(t, err)
		return buf.Bytes()
	}

	currentTime := time.Now()

	entity1 := genKeyWithMaterial(uid, currentTime, signingPriv, encPriv)
	privKey1 := encode(entity1)

	currentTime2 := currentTime.Add(-24 * time.Hour)
	entity2 := genKeyWithMaterial(uid, currentTime2, signingPriv, encPriv)
	privKey2 := encode(entity2)

	tc := SetupEngineTest(t, "pgp")
	defer tc.Cleanup()

	user := CreateAndSignupFakeUser(tc, "pgp")
	secui := &libkb.TestSecretUI{Passphrase: user.Passphrase}
	uis := libkb.UIs{LogUI: tc.G.UI.GetLogUI(), SecretUI: secui}

	mctx := NewMetaContextForTest(tc).WithUIs(uis)

	var kid keybase1.KID

	{
		// Add first PGP key.
		eng, err := NewPGPKeyImportEngineFromBytes(tc.G, privKey1, false)
		require.NoError(t, err)
		err = RunEngine2(mctx, eng)
		require.NoError(t, err)
		kid = eng.bundle.GetKID()
	}

	{
		// Revoke that key.
		eng := NewRevokeKeyEngine(tc.G, kid)
		err := RunEngine2(mctx, eng)
		require.NoError(t, err)
	}

	var delegate2Err error
	{
		// Add second key. It should have the same KID as the first one.
		eng, err := NewPGPKeyImportEngineFromBytes(tc.G, privKey2, false)
		require.NoError(t, err)
		// Do not care about an error from this engine immediately, keep going.
		delegate2Err = RunEngine2(mctx, eng)
		kid2 := eng.bundle.GetKID()
		require.Equal(t, kid, kid2)
	}

	{
		// Try to identify that user
		tc := SetupEngineTest(t, "pgp")
		defer tc.Cleanup()

		idUI := &FakeIdentifyUI{}
		arg := keybase1.Identify2Arg{
			UserAssertion:    user.Username,
			UseDelegateUI:    false,
			CanSuppressUI:    true,
			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CLI,
		}

		uis := libkb.UIs{
			LogUI:      tc.G.UI.GetLogUI(),
			IdentifyUI: idUI,
		}
		eng := NewResolveThenIdentify2(tc.G, &arg)
		mctx := NewMetaContextForTest(tc).WithUIs(uis)
		err = RunEngine2(mctx, eng)
		require.NoError(t, err)
	}

	// Check PGP import engine error. When this bug was first reported, that
	// engine was erroring out but the key was still being added.
	require.NoError(t, delegate2Err)
}
