/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package main

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"testing"

	"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
	"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
	"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
	"github.com/google/go-tpm-tools/proto/attest"
	"github.com/google/go-tpm-tools/proto/tpm"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/goleak"
	"gopkg.in/yaml.v3"
)

func TestMain(m *testing.M) {
	goleak.VerifyTestMain(m)
}

func TestValidatePCRAttDoc(t *testing.T) {
	testCases := map[string]struct {
		attDocRaw []byte
		wantErr   bool
	}{
		"invalid attestation document": {
			attDocRaw: []byte{0x1, 0x2, 0x3},
			wantErr:   true,
		},
		"nil attestation": {
			attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{}),
			wantErr:   true,
		},
		"nil quotes": {
			attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{
				Attestation: &attest.Attestation{},
			}),
			wantErr: true,
		},
		"invalid PCRs": {
			attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{
				Attestation: &attest.Attestation{
					Quotes: []*tpm.Quote{
						{
							Pcrs: &tpm.PCRs{
								Hash: tpm.HashAlgo_SHA256,
								Pcrs: map[uint32][]byte{
									0: {0x1, 0x2, 0x3},
								},
							},
						},
					},
				},
			}),
			wantErr: true,
		},
		"valid PCRs": {
			attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{
				Attestation: &attest.Attestation{
					Quotes: []*tpm.Quote{
						{
							Pcrs: &tpm.PCRs{
								Hash: tpm.HashAlgo_SHA256,
								Pcrs: map[uint32][]byte{
									0: bytes.Repeat([]byte{0xAA}, 32),
								},
							},
						},
					},
				},
			}),
			wantErr: false,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			assert := assert.New(t)
			require := require.New(t)

			pcrs, err := validatePCRAttDoc(tc.attDocRaw)
			if tc.wantErr {
				assert.Error(err)
			} else {
				require.NoError(err)

				attDoc := vtpm.AttestationDocument{}
				require.NoError(json.Unmarshal(tc.attDocRaw, &attDoc))
				qIdx, err := vtpm.GetSHA256QuoteIndex(attDoc.Attestation.Quotes)
				require.NoError(err)

				for pcrIdx, pcrVal := range pcrs {
					assert.Equal(pcrVal.Expected[:], attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs[pcrIdx])
				}
			}
		})
	}
}

func mustMarshalAttDoc(t *testing.T, attDoc vtpm.AttestationDocument) []byte {
	attDocRaw, err := json.Marshal(attDoc)
	require.NoError(t, err)
	return attDocRaw
}

func TestPrintPCRs(t *testing.T) {
	testCases := map[string]struct {
		format string
	}{
		"json": {
			format: "json",
		},
		"empty format": {
			format: "",
		},
		"yaml": {
			format: "yaml",
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			assert := assert.New(t)

			pcrs := measurements.M{
				0: measurements.WithAllBytes(0xAA, true),
				1: measurements.WithAllBytes(0xBB, true),
				2: measurements.WithAllBytes(0xCC, true),
			}

			var out bytes.Buffer
			err := printPCRs(&out, pcrs, tc.format)
			assert.NoError(err)

			for idx, pcr := range pcrs {
				assert.Contains(out.String(), fmt.Sprintf("%d", idx))
				assert.Contains(out.String(), hex.EncodeToString(pcr.Expected[:]))
			}
		})
	}
}

func TestPrintPCRsWithMetadata(t *testing.T) {
	testCases := map[string]struct {
		format string
		csp    cloudprovider.Provider
		image  string
	}{
		"json": {
			format: "json",
			csp:    cloudprovider.Azure,
			image:  "v2.0.0",
		},
		"yaml": {
			csp:    cloudprovider.GCP,
			image:  "v2.0.0-testimage",
			format: "yaml",
		},
		"empty format": {
			format: "",
			csp:    cloudprovider.QEMU,
			image:  "v2.0.0-testimage",
		},
		"empty": {},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			assert := assert.New(t)
			require := require.New(t)

			pcrs := measurements.M{
				0: measurements.WithAllBytes(0xAA, true),
				1: measurements.WithAllBytes(0xBB, true),
				2: measurements.WithAllBytes(0xCC, true),
			}

			outputWithMetadata := measurements.WithMetadata{
				CSP:          tc.csp,
				Image:        tc.image,
				Measurements: pcrs,
			}

			var out bytes.Buffer
			err := printPCRsWithMetadata(&out, outputWithMetadata, tc.format)
			assert.NoError(err)

			var unmarshalledOutput measurements.WithMetadata
			if tc.format == "" || tc.format == "json" {
				require.NoError(json.Unmarshal(out.Bytes(), &unmarshalledOutput))
			} else if tc.format == "yaml" {
				require.NoError(yaml.Unmarshal(out.Bytes(), &unmarshalledOutput))
			}

			assert.NotNil(unmarshalledOutput.CSP)
			assert.NotNil(unmarshalledOutput.Image)
			assert.Equal(tc.csp, unmarshalledOutput.CSP)
			assert.Equal(tc.image, unmarshalledOutput.Image)

			for idx, pcr := range pcrs {
				assert.Contains(out.String(), fmt.Sprintf("%d", idx))
				assert.Contains(out.String(), hex.EncodeToString(pcr.Expected[:]))
			}
		})
	}
}
