package verify

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/DataDog/go-tuf/data"
	"github.com/DataDog/go-tuf/pkg/keys"
	"github.com/DataDog/go-tuf/sign"
	"golang.org/x/crypto/ed25519"

	. "gopkg.in/check.v1"
)

// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }

type VerifySuite struct{}

var _ = Suite(&VerifySuite{})

type ecdsaSigner struct {
	*ecdsa.PrivateKey
}

type ecdsaPublic struct {
	PublicKey *keys.PKIXPublicKey `json:"public"`
}

func (s ecdsaSigner) PublicData() *data.PublicKey {
	keyValBytes, _ := json.Marshal(ecdsaPublic{PublicKey: &keys.PKIXPublicKey{PublicKey: s.Public()}})
	return &data.PublicKey{
		Type:       data.KeyTypeECDSA_SHA2_P256,
		Scheme:     data.KeySchemeECDSA_SHA2_P256,
		Algorithms: data.HashAlgorithms,
		Value:      keyValBytes,
	}
}

func (s ecdsaSigner) SignMessage(message []byte) ([]byte, error) {
	hash := sha256.Sum256(message)
	return s.PrivateKey.Sign(rand.Reader, hash[:], crypto.SHA256)
}

func (s ecdsaSigner) ContainsID(id string) bool {
	return s.PublicData().ContainsID(id)
}

func (ecdsaSigner) MarshalPrivateKey() (*data.PrivateKey, error) {
	return nil, errors.New("not implemented for test")
}

func (ecdsaSigner) UnmarshalPrivateKey(key *data.PrivateKey) error {
	return errors.New("not implemented for test")
}

func (VerifySuite) Test(c *C) {
	type test struct {
		name  string
		keys  []*data.PublicKey
		roles map[string]*data.Role
		s     *data.Signed
		ver   int64
		exp   *time.Time
		typ   string
		role  string
		err   error
		mut   func(*test)
	}

	expiredTime := time.Now().Add(-time.Hour)
	minVer := int64(10)
	tests := []test{
		{
			name: "no signatures",
			mut:  func(t *test) { t.s.Signatures = []data.Signature{} },
			err:  ErrNoSignatures,
		},
		{
			name: "unknown role",
			role: "foo",
			err:  ErrUnknownRole{"foo"},
		},
		{
			// It is impossible to distinguish between an error of an invalid
			// signature and a threshold not achieved. Invalid signatures lead
			// to not achieving the threshold.
			name: "signature wrong length",
			mut:  func(t *test) { t.s.Signatures[0].Signature = []byte{0} },
			err:  ErrRoleThreshold{1, 0},
		},
		{
			name: "key missing from role",
			mut:  func(t *test) { t.roles["root"].KeyIDs = nil },
			err:  ErrRoleThreshold{1, 0},
		},
		{
			name: "invalid signature",
			mut:  func(t *test) { t.s.Signatures[0].Signature = make([]byte, ed25519.SignatureSize) },
			err:  ErrRoleThreshold{1, 0},
		},
		{
			name: "enough signatures with extra invalid signature",
			mut: func(t *test) {
				t.s.Signatures = append(t.s.Signatures, data.Signature{
					KeyID:     t.s.Signatures[0].KeyID,
					Signature: make([]byte, ed25519.SignatureSize)})
			},
		},
		{
			name: "not enough signatures",
			mut:  func(t *test) { t.roles["root"].Threshold = 2 },
			err:  ErrRoleThreshold{2, 1},
		},
		{
			name: "exactly enough signatures",
		},
		{
			name: "more than enough signatures",
			mut: func(t *test) {
				k, _ := keys.GenerateEd25519Key()
				sign.Sign(t.s, k)
				t.keys = append(t.keys, k.PublicData())
				t.roles["root"].KeyIDs = append(t.roles["root"].KeyIDs, k.PublicData().IDs()...)
			},
		},
		{
			name: "duplicate key id",
			mut: func(t *test) {
				t.roles["root"].Threshold = 2
				t.s.Signatures = append(t.s.Signatures, t.s.Signatures[0])
			},
			err: ErrRoleThreshold{2, 1},
		},
		{
			name: "unknown key",
			mut: func(t *test) {
				k, _ := keys.GenerateEd25519Key()
				sign.Sign(t.s, k)
			},
		},
		{
			name: "unknown key below threshold",
			mut: func(t *test) {
				k, _ := keys.GenerateEd25519Key()
				sign.Sign(t.s, k)
				t.roles["root"].Threshold = 2
			},
			err: ErrRoleThreshold{2, 1},
		},
		{
			name: "unknown keys in db",
			mut: func(t *test) {
				k, _ := keys.GenerateEd25519Key()
				sign.Sign(t.s, k)
				t.keys = append(t.keys, k.PublicData())
			},
		},
		{
			name: "unknown keys in db below threshold",
			mut: func(t *test) {
				k, _ := keys.GenerateEd25519Key()
				sign.Sign(t.s, k)
				t.keys = append(t.keys, k.PublicData())
				t.roles["root"].Threshold = 2
			},
			err: ErrRoleThreshold{2, 1},
		},
		{
			name: "wrong type",
			typ:  "bar",
			err:  ErrWrongMetaType,
		},
		{
			name: "low version",
			ver:  minVer - 1,
			err:  ErrLowVersion{minVer - 1, minVer},
		},
		{
			name: "expired",
			exp:  &expiredTime,
			err:  ErrExpired{expiredTime},
		},
		{
			name: "valid ecdsa signature",
			mut: func(t *test) {
				k, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
				s := ecdsaSigner{k}
				sign.Sign(t.s, s)
				t.s.Signatures = t.s.Signatures[1:]
				t.keys = []*data.PublicKey{s.PublicData()}
				t.roles["root"].KeyIDs = s.PublicData().IDs()
			},
		},
		{
			// The threshold is still achieved.
			name: "invalid second ecdsa signature",
			mut: func(t *test) {
				k, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
				s := ecdsaSigner{k}
				sign.Sign(t.s, s)
				t.s.Signatures[1].Signature[0]++
				t.keys = append(t.keys, s.PublicData())
				t.roles["root"].KeyIDs = append(t.roles["root"].KeyIDs, s.PublicData().IDs()...)
			},
		},
	}
	for _, t := range tests {
		if t.role == "" {
			t.role = "root"
		}
		if t.ver == 0 {
			t.ver = minVer
		}
		if t.exp == nil {
			expires := time.Now().Add(time.Hour)
			t.exp = &expires
		}
		if t.typ == "" {
			t.typ = t.role
		}
		if t.keys == nil && t.s == nil {
			k, _ := keys.GenerateEd25519Key()
			t.s, _ = sign.Marshal(&signedMeta{Type: t.typ, Version: t.ver, Expires: *t.exp}, k)
			t.keys = []*data.PublicKey{k.PublicData()}
		}
		if t.roles == nil {
			t.roles = map[string]*data.Role{
				"root": {
					KeyIDs:    t.keys[0].IDs(),
					Threshold: 1,
				},
			}
		}
		if t.mut != nil {
			t.mut(&t)
		}

		db := NewDB()
		for _, k := range t.keys {
			for _, id := range k.IDs() {
				err := db.AddKey(id, k)
				c.Assert(err, IsNil)
			}
		}
		for n, r := range t.roles {
			err := db.AddRole(n, r)
			c.Assert(err, IsNil)
		}

		err := db.Verify(t.s, t.role, minVer)
		if e, ok := t.err.(ErrExpired); ok {
			assertErrExpired(c, err, e)
		} else {
			c.Assert(err, DeepEquals, t.err, Commentf("name = %s", t.name))
		}
	}
}

func (VerifySuite) TestVerifyIgnoreExpired(c *C) {
	minVer := int64(10)
	role := "root"
	k, _ := keys.GenerateEd25519Key()
	s, _ := sign.Marshal(&signedMeta{Type: role, Version: minVer, Expires: time.Now().Add(-time.Hour)}, k)
	keys := []*data.PublicKey{k.PublicData()}
	roles := map[string]*data.Role{
		"root": {
			KeyIDs:    keys[0].IDs(),
			Threshold: 1,
		},
	}

	db := NewDB()
	for _, k := range keys {
		for _, id := range k.IDs() {
			err := db.AddKey(id, k)
			c.Assert(err, IsNil)
		}
	}
	for n, r := range roles {
		err := db.AddRole(n, r)
		c.Assert(err, IsNil)
	}

	err := db.VerifyIgnoreExpiredCheck(s, role, minVer)
	c.Assert(err, IsNil)
}

func assertErrExpired(c *C, err error, expected ErrExpired) {
	actual, ok := err.(ErrExpired)
	if !ok {
		c.Fatalf("expected err to have type ErrExpired, got %T", err)
	}
	c.Assert(actual.Expired.Unix(), Equals, expected.Expired.Unix())
}
