// Copyright 2022 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build e2e
// +build e2e

package e2e

import (
	"bytes"
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/in-toto/in-toto-golang/in_toto"
	"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
	slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
	"github.com/secure-systems-lab/go-securesystemslib/dsse"
	"github.com/sigstore/rekor/pkg/generated/models"
	e2ex509 "github.com/sigstore/rekor/pkg/pki/x509/e2ex509"
	"github.com/sigstore/rekor/pkg/sharding"
	"github.com/sigstore/rekor/pkg/types"
	"github.com/sigstore/sigstore/pkg/signature"
)

type StoredEntry struct {
	Attestation string
	UUID        string
}

// Make sure we can add an entry
func TestHarnessAddEntry(t *testing.T) {
	// Create a random artifact and sign it.
	artifactPath := filepath.Join(t.TempDir(), "artifact")
	sigPath := filepath.Join(t.TempDir(), "signature.asc")

	e2ex509.CreatedX509SignedArtifact(t, artifactPath, sigPath)
	dataBytes, _ := ioutil.ReadFile(artifactPath)
	h := sha256.Sum256(dataBytes)
	dataSHA := hex.EncodeToString(h[:])

	// Write the public key to a file
	pubPath := filepath.Join(t.TempDir(), "pubKey.asc")
	if err := ioutil.WriteFile(pubPath, []byte(e2ex509.RSACert), 0644); err != nil {
		t.Fatal(err)
	}

	// Verify should fail initially
	runCliErr(t, "verify", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)

	// It should upload successfully.
	out := runCli(t, "upload", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
	outputContains(t, out, "Created entry at")
	uuid := getUUIDFromUploadOutput(t, out)
	logIndex := getLogIndexFromUploadOutput(t, out)

	if !rekorCLIIncompatible() {
		// Now we should be able to verify it.
		out = runCli(t, "verify", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
		outputContains(t, out, "Inclusion Proof:")
	}

	saveEntry(t, logIndex, StoredEntry{UUID: uuid})
}

// Make sure we can add an intoto entry
func TestHarnessAddIntoto(t *testing.T) {
	td := t.TempDir()
	attestationPath := filepath.Join(td, "attestation.json")
	pubKeyPath := filepath.Join(td, "pub.pem")

	// Get some random data so it's unique each run
	d := randomData(t, 10)
	id := base64.StdEncoding.EncodeToString(d)

	it := in_toto.ProvenanceStatement{
		StatementHeader: in_toto.StatementHeader{
			Type:          in_toto.StatementInTotoV01,
			PredicateType: slsa.PredicateSLSAProvenance,
			Subject: []in_toto.Subject{
				{
					Name: "foobar",
					Digest: common.DigestSet{
						"foo": "bar",
					},
				},
			},
		},
		Predicate: slsa.ProvenancePredicate{
			Builder: common.ProvenanceBuilder{
				ID: "foo" + id,
			},
		},
	}

	b, err := json.Marshal(it)
	if err != nil {
		t.Fatal(err)
	}

	pb, _ := pem.Decode([]byte(e2ex509.ECDSAPriv))
	priv, err := x509.ParsePKCS8PrivateKey(pb.Bytes)
	if err != nil {
		t.Fatal(err)
	}

	s, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256)
	if err != nil {
		t.Fatal(err)
	}

	signer, err := dsse.NewEnvelopeSigner(&e2ex509.Verifier{
		S: s,
	})
	if err != nil {
		t.Fatal(err)
	}

	env, err := signer.SignPayload(context.Background(), "application/vnd.in-toto+json", b)
	if err != nil {
		t.Fatal(err)
	}

	eb, err := json.Marshal(env)
	if err != nil {
		t.Fatal(err)
	}

	write(t, string(eb), attestationPath)
	write(t, e2ex509.ECDSAPub, pubKeyPath)

	// If we do it twice, it should already exist
	out := runCliStdout(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath)
	outputContains(t, out, "Created entry at")
	uuid := getUUIDFromUploadOutput(t, out)
	logIndex := getLogIndexFromUploadOutput(t, out)

	out = runCli(t, "get", "--log-index", fmt.Sprintf("%d", logIndex), "--format=json")
	g := getOut{}
	if err := json.Unmarshal([]byte(out), &g); err != nil {
		t.Fatal(err)
	}
	// The attestation should be stored at /var/run/attestations/sha256:digest

	got := in_toto.ProvenanceStatement{}
	if err := json.Unmarshal([]byte(g.Attestation), &got); err != nil {
		t.Fatal(err)
	}
	if diff := cmp.Diff(it, got); diff != "" {
		t.Errorf("diff: %s", diff)
	}

	attHash := sha256.Sum256(b)

	intotoModel := &models.IntotoV002Schema{}
	if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoModel); err != nil {
		t.Errorf("could not convert body into intoto type: %v", err)
	}
	if intotoModel.Content == nil || intotoModel.Content.PayloadHash == nil {
		t.Errorf("could not find hash over attestation %v", intotoModel)
	}
	recordedPayloadHash, err := hex.DecodeString(*intotoModel.Content.PayloadHash.Value)
	if err != nil {
		t.Errorf("error converting attestation hash to []byte: %v", err)
	}

	if !bytes.Equal(attHash[:], recordedPayloadHash) {
		t.Fatal(fmt.Errorf("attestation hash %v doesnt match the payload we sent %v", hex.EncodeToString(attHash[:]),
			*intotoModel.Content.PayloadHash.Value))
	}

	out = runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath)
	outputContains(t, out, "Entry already exists")
	saveEntry(t, logIndex, StoredEntry{Attestation: g.Attestation, UUID: uuid})
}

func getEntries(t *testing.T) (string, map[int]StoredEntry) {
	tmpDir := os.Getenv("REKOR_HARNESS_TMPDIR")
	if tmpDir == "" {
		t.Skip("Skipping test, REKOR_HARNESS_TMPDIR is not set")
	}
	file := filepath.Join(tmpDir, "attestations")

	t.Log("Reading", file)
	attestations := map[int]StoredEntry{}
	contents, err := os.ReadFile(file)
	if errors.Is(err, os.ErrNotExist) || contents == nil {
		return file, attestations
	}
	if err != nil {
		t.Fatal(err)
	}
	if err := json.Unmarshal(contents, &attestations); err != nil {
		t.Fatal(err)
	}
	return file, attestations
}

func saveEntry(t *testing.T, logIndex int, entry StoredEntry) {
	file, attestations := getEntries(t)
	t.Logf("Storing entry for logIndex %d", logIndex)
	attestations[logIndex] = entry
	contents, err := json.Marshal(attestations)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(file, contents, 0777); err != nil {
		t.Fatal(err)
	}
}

func compareAttestation(t *testing.T, logIndex int, got string) {
	_, entries := getEntries(t)
	expected, ok := entries[logIndex]
	if !ok {
		t.Fatalf("expected to find persisted entries with logIndex %d but none existed: %v", logIndex, entries)
	}

	if got != expected.Attestation {
		t.Fatalf("attestations don't match, got %v expected %v", got, expected)
	}
}

// Make sure we can get and verify all entries
// For attestations, make sure we can see the attestation
// Older versions of the CLI may not be able to parse the retrieved entry.
func TestHarnessGetAllEntriesLogIndex(t *testing.T) {
	if rekorCLIIncompatible() {
		t.Skipf("Skipping getting entries by UUID, old rekor-cli version %s is incompatible with server version %s", os.Getenv("CLI_VERSION"), os.Getenv("SERVER_VERSION"))
	}

	treeSize := activeTreeSize(t)
	if treeSize == 0 {
		t.Fatal("There are 0 entries in the log, there should be at least 2")
	}
	for i := 0; i < treeSize; i++ {
		out := runCli(t, "get", "--log-index", fmt.Sprintf("%d", i), "--format", "json")
		if !strings.Contains(out, "IntotoObj") {
			continue
		}
		var intotoObj struct {
			Attestation string
		}
		if err := json.Unmarshal([]byte(out), &intotoObj); err != nil {
			t.Fatal(err)
		}
		compareAttestation(t, i, intotoObj.Attestation)
		t.Log("IntotoObj matches stored attestation")
	}
}

func TestHarnessGetAllEntriesUUID(t *testing.T) {
	if rekorCLIIncompatible() {
		t.Skipf("Skipping getting entries by UUID, old rekor-cli version %s is incompatible with server version %s", os.Getenv("CLI_VERSION"), os.Getenv("SERVER_VERSION"))
	}

	treeSize := activeTreeSize(t)
	if treeSize == 0 {
		t.Fatal("There are 0 entries in the log, there should be at least 2")
	}
	_, entries := getEntries(t)

	for _, e := range entries {
		outUUID := runCli(t, "get", "--uuid", e.UUID, "--format", "json")
		outEntryID := runCli(t, "get", "--uuid", entryID(t, e.UUID), "--format", "json")

		if outUUID != outEntryID {
			t.Fatalf("Getting by uuid %s and entryID %s gave different outputs:\nuuid: %v\nentryID:%v\n", e.UUID, entryID(t, e.UUID), outUUID, outEntryID)
		}

		if !strings.Contains(outUUID, "IntotoObj") {
			continue
		}
		var intotoObj struct {
			Attestation string
		}
		if err := json.Unmarshal([]byte(outUUID), &intotoObj); err != nil {
			t.Fatal(err)
		}
		if intotoObj.Attestation != e.Attestation {
			t.Fatalf("attestations don't match, got %v expected %v", intotoObj.Attestation, e.Attestation)
		}
	}
}

func entryID(t *testing.T, uuid string) string {
	if sharding.ValidateEntryID(uuid) == nil {
		return uuid
	}
	treeID, err := strconv.Atoi(os.Getenv("TREE_ID"))
	if err != nil {
		t.Fatal(err)
	}
	tid := strconv.FormatInt(int64(treeID), 16)
	ts, err := sharding.PadToTreeIDLen(tid)
	if err != nil {
		t.Fatal(err)
	}
	return ts + uuid
}

func activeTreeSize(t *testing.T) int {
	out := runCliStdout(t, "loginfo", "--format", "json", "--store_tree_state", "false")
	t.Log(string(out))
	var s struct {
		ActiveTreeSize int
	}
	if err := json.Unmarshal([]byte(out), &s); err != nil {
		t.Fatal(err)
	}
	return s.ActiveTreeSize
}

// Check if we have a new server version and an old CLI version
// since the new server returns an EntryID but the old CLI version expects a UUID
// Also, new rekor server allows upload of intoto v0.0.2, and old rekor cli versions
// don't understand how to parse these entries.
// TODO: use semver comparisons.
func rekorCLIIncompatible() bool {
	if sv := os.Getenv("SERVER_VERSION"); sv != "v0.10.0" && sv != "v0.11.0" {
		if cv := os.Getenv("CLI_VERSION"); cv == "v0.10.0" || cv == "v0.11.0" {
			return true
		}
	}

	return false
}
