//
// 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.

package verify

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"testing"

	"github.com/go-openapi/runtime"
	"github.com/go-openapi/strfmt"
	"github.com/go-openapi/swag/conv"
	"github.com/sigstore/rekor/pkg/generated/client"
	"github.com/sigstore/rekor/pkg/generated/client/tlog"
	"github.com/sigstore/rekor/pkg/generated/models"
	"github.com/sigstore/rekor/pkg/util"
	"github.com/sigstore/sigstore/pkg/signature"
)

type TlogClient struct {
	Proof   []string
	Root    string
	LogInfo models.LogInfo
}

func (m *TlogClient) GetLogProof(_ *tlog.GetLogProofParams, _ ...tlog.ClientOption) (*tlog.GetLogProofOK, error) {
	return &tlog.GetLogProofOK{
		Payload: &models.ConsistencyProof{
			Hashes:   m.Proof,
			RootHash: &m.Root,
		}}, nil
}

func (m *TlogClient) GetLogInfo(_ *tlog.GetLogInfoParams, _ ...tlog.ClientOption) (*tlog.GetLogInfoOK, error) {
	return &tlog.GetLogInfoOK{
		Payload: &m.LogInfo,
	}, nil
}

// TODO: Implement mock
func (m *TlogClient) SetTransport(_ runtime.ClientTransport) {
}

func TestConsistency(t *testing.T) {
	root2String := "5be1758dd2228acfaf2546b4b6ce8aa40c82a3748f3dcb550e0d67ba34f02a45"
	root2, _ := hex.DecodeString(root2String)
	root1, _ := hex.DecodeString("59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c")
	root0, _ := hex.DecodeString("1a341bc342ff4e567387de9789ab14000b147124317841489172419874198147")
	hashes := []string{"d3be742c8d73e2dd3c5635843e987ad3dfb3837616f412a07bf730c3ad73f5cb"}
	for _, test := range []struct {
		name    string
		oldC    util.Checkpoint
		newC    util.Checkpoint
		Proof   []string
		wantErr bool
	}{
		{
			name: "zero length proof",
			oldC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(2),
				Hash:   root2,
			},
			newC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(2),
				Hash:   root2,
			},
			wantErr: false,
		},
		{
			name: "valid consistency proof",
			oldC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(1),
				Hash:   root1,
			},
			newC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(2),
				Hash:   root2,
			},
			wantErr: false,
		},
		{
			name: "invalid new sth request",
			oldC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(2),
				Hash:   root1,
			},
			newC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(1),
				Hash:   root2,
			},
			wantErr: true,
		},
		{
			name: "invalid consistency proof",
			oldC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(1),
				Hash:   root2,
			},
			newC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(2),
				Hash:   root1,
			},
			wantErr: true,
		},
		{
			name: "invalid consistency - same size",
			oldC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(1),
				Hash:   root1,
			},
			newC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(1),
				Hash:   root2,
			},
			wantErr: true,
		},
		{
			name: "invalid consistency - empty log",
			oldC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(0),
				Hash:   root0,
			},
			newC: util.Checkpoint{
				Origin: "test",
				Size:   uint64(2),
				Hash:   root2,
			},
			wantErr: true,
		},
	} {
		var mClient client.Rekor
		mClient.Tlog = &TlogClient{Proof: hashes, Root: root2String}

		t.Run(string(test.name), func(t *testing.T) {

			ctx := context.Background()
			treeID := "123"
			oldSTH, err := util.CreateSignedCheckpoint(test.oldC)
			if err != nil {
				t.Fatalf("creating old checkpoint")
			}
			newSTH, err := util.CreateSignedCheckpoint(test.newC)
			if err != nil {
				t.Fatalf("creating new checkpoint")
			}

			gotErr := ProveConsistency(ctx, &mClient, oldSTH, newSTH, treeID)

			if (gotErr != nil) != test.wantErr {
				t.Fatalf("ProveConsistency = %t, wantErr %t", gotErr, test.wantErr)
			}
		})
	}
}

func TestInclusion(t *testing.T) {
	time := int64(1661794812)
	logID := "1701474e8cb504dbb853a5887bc2cf66936b0f36d2641bfb61f1abae80088e6a"
	for _, test := range []struct {
		name    string
		e       models.LogEntryAnon
		wantErr bool
	}{
		{
			name: "valid inclusion",
			e: models.LogEntryAnon{
				Body:           "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlY2RjNTUzNmY3M2JkYWU4ODE2ZjBlYTQwNzI2ZWY1ZTliODEwZDkxNDQ5MzA3NTkwM2JiOTA2MjNkOTdiMWQ4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUQvUGRQUW1LV0MxKzBCTkVkNWdLdlFHcjF4eGwzaWVVZmZ2M2prMXp6Skt3SWhBTEJqM3hmQXlXeGx6NGpwb0lFSVYxVWZLOXZua1VVT1NvZVp4QlpQSEtQQyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRVOWpWR1pTUWxNNWFtbFlUVGd4UmxvNFoyMHZNU3R2YldWTmR3cHRiaTh6TkRjdk5UVTJaeTlzY21sVE56SjFUV2haT1V4alZDczFWVW8yWmtkQ1oyeHlOVm80VERCS1RsTjFZWE41WldRNVQzUmhVblozUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19",
				IntegratedTime: &time,
				LogID:          &logID,
				LogIndex:       conv.Pointer(int64(1)),
				Verification: &models.LogEntryAnonVerification{
					InclusionProof: &models.InclusionProof{
						TreeSize: conv.Pointer(int64(2)),
						RootHash: conv.Pointer("5be1758dd2228acfaf2546b4b6ce8aa40c82a3748f3dcb550e0d67ba34f02a45"),
						LogIndex: conv.Pointer(int64(1)),
						Hashes: []string{
							"59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c",
						},
					},
					SignedEntryTimestamp: strfmt.Base64("MEUCIHJj8xP+oPTd4BAXhO2lcbRplnKW2FafMiFo0gIDGUcYAiEA80BJ8QikiupGAv3R3dtSvZ1ICsAOQat10cFKPqBkLBM="),
				},
			},
			wantErr: false,
		},
		{
			name: "invalid inclusion - bad body hash",
			e: models.LogEntryAnon{
				Body:           "ayJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlY2RjNTUzNmY3M2JkYWU4ODE2ZjBlYTQwNzI2ZWY1ZTliODEwZDkxNDQ5MzA3NTkwM2JiOTA2MjNkOTdiMWQ4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUQvUGRQUW1LV0MxKzBCTkVkNWdLdlFHcjF4eGwzaWVVZmZ2M2prMXp6Skt3SWhBTEJqM3hmQXlXeGx6NGpwb0lFSVYxVWZLOXZua1VVT1NvZVp4QlpQSEtQQyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRVOWpWR1pTUWxNNWFtbFlUVGd4UmxvNFoyMHZNU3R2YldWTmR3cHRiaTh6TkRjdk5UVTJaeTlzY21sVE56SjFUV2haT1V4alZDczFWVW8yWmtkQ1oyeHlOVm80VERCS1RsTjFZWE41WldRNVQzUmhVblozUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19",
				IntegratedTime: &time,
				LogID:          &logID,
				LogIndex:       conv.Pointer(int64(1)),
				Verification: &models.LogEntryAnonVerification{
					InclusionProof: &models.InclusionProof{
						TreeSize: conv.Pointer(int64(2)),
						RootHash: conv.Pointer("5be1758dd2228acfaf2546b4b6ce8aa40c82a3748f3dcb550e0d67ba34f02a45"),
						LogIndex: conv.Pointer(int64(1)),
						Hashes: []string{
							"59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c",
						},
					},
					SignedEntryTimestamp: strfmt.Base64("MEUCIHJj8xP+oPTd4BAXhO2lcbRplnKW2FafMiFo0gIDGUcYAiEA80BJ8QikiupGAv3R3dtSvZ1ICsAOQat10cFKPqBkLBM="),
				},
			},
			wantErr: true,
		},
		{
			name: "invalid inclusion - body not string",
			e: models.LogEntryAnon{
				Body:           123,
				IntegratedTime: &time,
				LogID:          &logID,
				LogIndex:       conv.Pointer(int64(1)),
				Verification: &models.LogEntryAnonVerification{
					InclusionProof: &models.InclusionProof{
						TreeSize: conv.Pointer(int64(2)),
						RootHash: conv.Pointer("5be1758dd2228acfaf2546b4b6ce8aa40c82a3748f3dcb550e0d67ba34f02a45"),
						LogIndex: conv.Pointer(int64(1)),
						Hashes: []string{
							"59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c",
						},
					},
					SignedEntryTimestamp: strfmt.Base64("MEUCIHJj8xP+oPTd4BAXhO2lcbRplnKW2FafMiFo0gIDGUcYAiEA80BJ8QikiupGAv3R3dtSvZ1ICsAOQat10cFKPqBkLBM="),
				},
			},
			wantErr: true,
		},
	} {
		t.Run(string(test.name), func(t *testing.T) {
			ctx := context.Background()

			gotErr := VerifyInclusion(ctx, &test.e)

			if (gotErr != nil) != test.wantErr {
				t.Fatalf("VerifyInclusion = %s, wantErr %t", gotErr.Error(), test.wantErr)
			}
		})
	}
}

func TestCheckpoint(t *testing.T) {
	hostname := "rekor.localhost"
	treeID := int64(123)
	rootHash := sha256.Sum256([]byte{1, 2, 3})
	rootHashString := hex.EncodeToString(rootHash[:])
	treeSize := uint64(42)
	signer, _, err := signature.NewDefaultECDSASignerVerifier()
	if err != nil {
		t.Fatalf("error generating signer: %v", err)
	}
	ctx := context.Background()
	scBytes, err := util.CreateAndSignCheckpoint(ctx, hostname, treeID, treeSize, rootHash[:], signer)
	if err != nil {
		t.Fatalf("error creating signed checkpoint: %v", err)
	}

	time := int64(1661794812)
	logID := "1701474e8cb504dbb853a5887bc2cf66936b0f36d2641bfb61f1abae80088e6a"

	for _, test := range []struct {
		name    string
		e       models.LogEntryAnon
		wantErr bool
	}{
		{
			name: "valid checkpoint",
			e: models.LogEntryAnon{
				Body:           "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlY2RjNTUzNmY3M2JkYWU4ODE2ZjBlYTQwNzI2ZWY1ZTliODEwZDkxNDQ5MzA3NTkwM2JiOTA2MjNkOTdiMWQ4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUQvUGRQUW1LV0MxKzBCTkVkNWdLdlFHcjF4eGwzaWVVZmZ2M2prMXp6Skt3SWhBTEJqM3hmQXlXeGx6NGpwb0lFSVYxVWZLOXZua1VVT1NvZVp4QlpQSEtQQyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRVOWpWR1pTUWxNNWFtbFlUVGd4UmxvNFoyMHZNU3R2YldWTmR3cHRiaTh6TkRjdk5UVTJaeTlzY21sVE56SjFUV2haT1V4alZDczFWVW8yWmtkQ1oyeHlOVm80VERCS1RsTjFZWE41WldRNVQzUmhVblozUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19",
				IntegratedTime: &time,
				LogID:          &logID,
				LogIndex:       conv.Pointer(int64(1)),
				Verification: &models.LogEntryAnonVerification{
					InclusionProof: &models.InclusionProof{
						TreeSize: conv.Pointer(int64(2)),
						RootHash: conv.Pointer(rootHashString),
						LogIndex: conv.Pointer(int64(1)),
						Hashes: []string{
							"59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c",
						},
						Checkpoint: conv.Pointer(string(scBytes)),
					},
					SignedEntryTimestamp: strfmt.Base64("MEUCIHJj8xP+oPTd4BAXhO2lcbRplnKW2FafMiFo0gIDGUcYAiEA80BJ8QikiupGAv3R3dtSvZ1ICsAOQat10cFKPqBkLBM="),
				},
			},
			wantErr: false,
		},
		{
			name: "root hash mismatch",
			e: models.LogEntryAnon{
				Body:           "ayJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlY2RjNTUzNmY3M2JkYWU4ODE2ZjBlYTQwNzI2ZWY1ZTliODEwZDkxNDQ5MzA3NTkwM2JiOTA2MjNkOTdiMWQ4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUQvUGRQUW1LV0MxKzBCTkVkNWdLdlFHcjF4eGwzaWVVZmZ2M2prMXp6Skt3SWhBTEJqM3hmQXlXeGx6NGpwb0lFSVYxVWZLOXZua1VVT1NvZVp4QlpQSEtQQyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRVOWpWR1pTUWxNNWFtbFlUVGd4UmxvNFoyMHZNU3R2YldWTmR3cHRiaTh6TkRjdk5UVTJaeTlzY21sVE56SjFUV2haT1V4alZDczFWVW8yWmtkQ1oyeHlOVm80VERCS1RsTjFZWE41WldRNVQzUmhVblozUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19",
				IntegratedTime: &time,
				LogID:          &logID,
				LogIndex:       conv.Pointer(int64(1)),
				Verification: &models.LogEntryAnonVerification{
					InclusionProof: &models.InclusionProof{
						TreeSize: conv.Pointer(int64(2)),
						RootHash: conv.Pointer("5be1758dd2228acfaf2546b4b6ce8aa40c82a3748f3dcb550e0d67ba34f02a45"),
						LogIndex: conv.Pointer(int64(1)),
						Hashes: []string{
							"59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c",
						},
						Checkpoint: conv.Pointer(string(scBytes)),
					},
					SignedEntryTimestamp: strfmt.Base64("MEUCIHJj8xP+oPTd4BAXhO2lcbRplnKW2FafMiFo0gIDGUcYAiEA80BJ8QikiupGAv3R3dtSvZ1ICsAOQat10cFKPqBkLBM="),
				},
			},
			wantErr: true,
		},
	} {
		t.Run(string(test.name), func(t *testing.T) {
			gotErr := VerifyCheckpointSignature(&test.e, signer)

			if (gotErr != nil) != test.wantErr {
				t.Fatalf("VerifyCheckpointSignature = %t, wantErr %t", gotErr, test.wantErr)
			}
		})
	}
}

func TestCheckpointRootHashCaseFolding(t *testing.T) {
	signer, _, err := signature.NewDefaultECDSASignerVerifier()
	if err != nil {
		t.Fatal(err)
	}

	var rootHash [32]byte
	for i := range rootHash {
		rootHash[i] = byte(i) + 1
	}
	rootHash[0] = 'A'

	scBytes, err := util.CreateAndSignCheckpoint(context.Background(), "rekor.localhost", 123, 42, rootHash[:], signer)
	if err != nil {
		t.Fatal(err)
	}

	// Same bytes except an ASCII letter in different case: not equal, but equal
	// under case folding.
	mismatch := rootHash
	mismatch[0] = 'a'

	e := models.LogEntryAnon{
		Verification: &models.LogEntryAnonVerification{
			InclusionProof: &models.InclusionProof{
				RootHash:   conv.Pointer(hex.EncodeToString(mismatch[:])),
				Checkpoint: conv.Pointer(string(scBytes)),
			},
		},
	}

	if err := VerifyCheckpointSignature(&e, signer); err == nil {
		t.Fatal("VerifyCheckpointSignature accepted a root hash that does not match the signed tree head")
	}
}
