// Copyright 2017 Google LLC. All Rights Reserved.
//
// 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 client_test

import (
	"context"
	"encoding/pem"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	ct "github.com/google/certificate-transparency-go"
	"github.com/google/certificate-transparency-go/client"
	"github.com/google/certificate-transparency-go/client/configpb"
	"github.com/google/certificate-transparency-go/testdata"
	"github.com/google/certificate-transparency-go/x509"
	"github.com/google/certificate-transparency-go/x509util"
	tspb "google.golang.org/protobuf/types/known/timestamppb"
)

func TestNewTemporalLogClient(t *testing.T) {
	ts0 := tspb.New(time.Date(2010, 9, 19, 11, 00, 00, 00, time.UTC))
	ts1 := tspb.New(time.Date(2011, 9, 19, 11, 00, 00, 00, time.UTC))
	ts2 := tspb.New(time.Date(2012, 9, 19, 11, 00, 00, 00, time.UTC))
	ts25 := tspb.New(time.Date(2013, 3, 19, 11, 00, 00, 00, time.UTC))
	ts3 := tspb.New(time.Date(2013, 9, 19, 11, 00, 00, 00, time.UTC))
	ts4 := tspb.New(time.Date(2014, 9, 19, 11, 00, 00, 00, time.UTC))

	tests := []struct {
		cfg     *configpb.TemporalLogConfig
		wantErr string
	}{
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: nil, NotAfterLimit: nil},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
					{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: nil, NotAfterLimit: ts1},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
					{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: nil, NotAfterLimit: ts1},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
					{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
					{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
					{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
					{Uri: "threeA", NotAfterStart: ts25, NotAfterLimit: ts3},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
				},
			},
			wantErr: "previous interval ended at",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
					{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
				},
			},
			wantErr: "previous interval ended at",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: nil, NotAfterLimit: ts1},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts1},
					{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
				},
			},
			wantErr: "inverted",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: nil},
					{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
				},
			},
			wantErr: "no upper bound",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
					{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
					{Uri: "three", NotAfterStart: nil, NotAfterLimit: ts3},
					{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
				},
			},
			wantErr: "has no lower bound",
		},
		{
			wantErr: "empty",
		},
		{
			cfg:     &configpb.TemporalLogConfig{Shard: []*configpb.LogShardConfig{}},
			wantErr: "empty",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts1, NotAfterLimit: ts1},
				},
			},
			wantErr: "inverted",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts2, NotAfterLimit: ts1},
				},
			},
			wantErr: "inverted",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: &tspb.Timestamp{Seconds: -1, Nanos: -1}, NotAfterLimit: ts2},
				},
			},
			wantErr: "failed to parse",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: "one", NotAfterStart: ts1, NotAfterLimit: &tspb.Timestamp{Seconds: -1, Nanos: -1}},
				},
			},
			wantErr: "failed to parse",
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{
						Uri:           "one",
						NotAfterStart: nil,
						NotAfterLimit: nil,
						PublicKeyDer:  []byte{0x01, 0x02},
					},
				},
			},
			wantErr: "invalid public key",
		},
	}
	for _, test := range tests {
		_, err := client.NewTemporalLogClient(test.cfg, nil)
		if err != nil {
			if test.wantErr == "" {
				t.Errorf("NewTemporalLogClient(%+v)=nil,%v; want _,nil", test.cfg, err)
			} else if !strings.Contains(err.Error(), test.wantErr) {
				t.Errorf("NewTemporalLogClient(%+v)=nil,%v; want _,%q", test.cfg, err, test.wantErr)
			}
			continue
		}
		if test.wantErr != "" {
			t.Errorf("NewTemporalLogClient(%+v)=_, nil; want _,%q", test.cfg, test.wantErr)
		}
	}
}

func TestIndexByDate(t *testing.T) {
	time0 := time.Date(2010, 9, 19, 11, 00, 00, 00, time.UTC)
	time1 := time.Date(2011, 9, 19, 11, 00, 00, 00, time.UTC)
	time19 := time.Date(2012, 9, 19, 10, 59, 59, 00, time.UTC)
	time2 := time.Date(2012, 9, 19, 11, 00, 00, 00, time.UTC)
	time25 := time.Date(2013, 3, 19, 11, 00, 00, 00, time.UTC)
	time3 := time.Date(2013, 9, 19, 11, 00, 00, 00, time.UTC)
	time4 := time.Date(2014, 9, 19, 11, 00, 00, 00, time.UTC)

	ts0 := tspb.New(time0)
	ts1 := tspb.New(time1)
	ts2 := tspb.New(time2)
	ts3 := tspb.New(time3)
	ts4 := tspb.New(time4)

	allCfg := &configpb.TemporalLogConfig{
		Shard: []*configpb.LogShardConfig{
			{Uri: "zero", NotAfterStart: nil, NotAfterLimit: ts0},
			{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
			{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
			{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
			{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
			{Uri: "five", NotAfterStart: ts4, NotAfterLimit: nil},
		},
	}
	uptoCfg := &configpb.TemporalLogConfig{
		Shard: []*configpb.LogShardConfig{
			{Uri: "zero", NotAfterStart: nil, NotAfterLimit: ts0},
			{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
			{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
			{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
			{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
		},
	}
	fromCfg :=
		&configpb.TemporalLogConfig{
			Shard: []*configpb.LogShardConfig{
				{Uri: "zero", NotAfterStart: ts0, NotAfterLimit: ts1},
				{Uri: "one", NotAfterStart: ts1, NotAfterLimit: ts2},
				{Uri: "two", NotAfterStart: ts2, NotAfterLimit: ts3},
				{Uri: "three", NotAfterStart: ts3, NotAfterLimit: ts4},
				{Uri: "four", NotAfterStart: ts4, NotAfterLimit: nil},
			},
		}
	boundedCfg :=
		&configpb.TemporalLogConfig{
			Shard: []*configpb.LogShardConfig{
				{Uri: "zero", NotAfterStart: ts0, NotAfterLimit: ts1},
				{Uri: "one", NotAfterStart: ts1, NotAfterLimit: ts2},
				{Uri: "two", NotAfterStart: ts2, NotAfterLimit: ts3},
				{Uri: "three", NotAfterStart: ts3, NotAfterLimit: ts4},
			},
		}

	tests := []struct {
		cfg     *configpb.TemporalLogConfig
		when    time.Time
		want    int
		wantErr bool
	}{
		{cfg: allCfg, when: time.Date(2000, 9, 19, 11, 00, 00, 00, time.UTC), want: 0},
		{cfg: allCfg, when: time0, want: 1},
		{cfg: allCfg, when: time1, want: 2},
		{cfg: allCfg, when: time19, want: 2},
		{cfg: allCfg, when: time2, want: 3},
		{cfg: allCfg, when: time25, want: 3},
		{cfg: allCfg, when: time3, want: 4},
		{cfg: allCfg, when: time4, want: 5},
		{cfg: allCfg, when: time.Date(2015, 9, 19, 11, 00, 00, 00, time.UTC), want: 5},

		{cfg: uptoCfg, when: time.Date(2000, 9, 19, 11, 00, 00, 00, time.UTC), want: 0},
		{cfg: uptoCfg, when: time0, want: 1},
		{cfg: uptoCfg, when: time1, want: 2},
		{cfg: uptoCfg, when: time2, want: 3},
		{cfg: uptoCfg, when: time25, want: 3},
		{cfg: uptoCfg, when: time3, want: 4},
		{cfg: uptoCfg, when: time4, wantErr: true},
		{cfg: uptoCfg, when: time.Date(2015, 9, 19, 11, 00, 00, 00, time.UTC), wantErr: true},

		{cfg: fromCfg, when: time.Date(2000, 9, 19, 11, 00, 00, 00, time.UTC), wantErr: true},
		{cfg: fromCfg, when: time0, want: 0},
		{cfg: fromCfg, when: time1, want: 1},
		{cfg: fromCfg, when: time2, want: 2},
		{cfg: fromCfg, when: time25, want: 2},
		{cfg: fromCfg, when: time3, want: 3},
		{cfg: fromCfg, when: time4, want: 4},
		{cfg: fromCfg, when: time.Date(2015, 9, 19, 11, 00, 00, 00, time.UTC), want: 4},

		{cfg: boundedCfg, when: time.Date(2000, 9, 19, 11, 00, 00, 00, time.UTC), wantErr: true},
		{cfg: boundedCfg, when: time0, want: 0},
		{cfg: boundedCfg, when: time1, want: 1},
		{cfg: boundedCfg, when: time2, want: 2},
		{cfg: boundedCfg, when: time25, want: 2},
		{cfg: boundedCfg, when: time3, want: 3},
		{cfg: boundedCfg, when: time4, wantErr: true},
		{cfg: boundedCfg, when: time.Date(2015, 9, 19, 11, 00, 00, 00, time.UTC), wantErr: true},
	}
	for _, test := range tests {
		tlc, err := client.NewTemporalLogClient(test.cfg, nil)
		if err != nil {
			t.Errorf("NewTemporalLogClient(%+v)=nil, %v; want _,nil", test.cfg, err)
			continue
		}
		got, err := tlc.IndexByDate(test.when)
		if err != nil {
			if !test.wantErr {
				t.Errorf("NewTemporalLogClient(%+v).idxByDate()=%d,%v; want %d,nil", test.cfg, got, err, test.want)
			}
			continue
		}
		if test.wantErr {
			t.Errorf("NewTemporalLogClient(%+v).idxByDate(%v)=%d, nil; want _, 'no log found'", test.cfg, test.when, got)
		}
		if got != test.want {
			t.Errorf("NewTemporalLogClient(%+v).idxByDate(%v)=%d, nil; want %d, nil", test.cfg, test.when, got, test.want)
		}
	}
}

func TestTemporalAddChain(t *testing.T) {
	hs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/ct/v1/add-chain":
			data, _ := sctToJSON(testdata.TestCertProof)
			if _, err := w.Write(data); err != nil {
				t.Error(err)
			}
		case "/ct/v1/add-pre-chain":
			data, _ := sctToJSON(testdata.TestPreCertProof)
			if _, err := w.Write(data); err != nil {
				t.Error(err)
			}
		default:
			t.Fatalf("Incorrect URL path: %s", r.URL.Path)
		}
	}))
	defer hs.Close()

	cert, err := x509util.CertificateFromPEM([]byte(testdata.TestCertPEM))
	if err != nil {
		t.Fatalf("Failed to parse certificate from PEM: %v", err)
	}
	certChain := []ct.ASN1Cert{{Data: cert.Raw}}
	precert, err := x509util.CertificateFromPEM([]byte(testdata.TestPreCertPEM))
	if x509.IsFatal(err) {
		t.Fatalf("Failed to parse pre-certificate from PEM: %v", err)
	}
	issuer, err := x509util.CertificateFromPEM([]byte(testdata.CACertPEM))
	if x509.IsFatal(err) {
		t.Fatalf("Failed to parse issuer certificate from PEM: %v", err)
	}
	precertChain := []ct.ASN1Cert{{Data: precert.Raw}, {Data: issuer.Raw}}
	// Both have Not After = Jun  1 00:00:00 2022 GMT
	ts1 := tspb.New(time.Date(2022, 5, 19, 11, 00, 00, 00, time.UTC))
	ts2 := tspb.New(time.Date(2022, 6, 19, 11, 00, 00, 00, time.UTC))
	p, _ := pem.Decode([]byte(testdata.LogPublicKeyPEM))
	if p == nil {
		t.Fatalf("Failed to parse public key from PEM: %v", err)
	}

	tests := []struct {
		cfg     *configpb.TemporalLogConfig
		wantErr bool
	}{
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: hs.URL, NotAfterStart: nil, NotAfterLimit: nil, PublicKeyDer: p.Bytes},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: hs.URL, NotAfterStart: nil, NotAfterLimit: ts2, PublicKeyDer: p.Bytes},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: hs.URL, NotAfterStart: ts1, NotAfterLimit: nil, PublicKeyDer: p.Bytes},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: hs.URL, NotAfterStart: ts1, NotAfterLimit: ts2, PublicKeyDer: p.Bytes},
				},
			},
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: hs.URL, NotAfterStart: nil, NotAfterLimit: ts1, PublicKeyDer: p.Bytes},
				},
			},
			wantErr: true,
		},
		{
			cfg: &configpb.TemporalLogConfig{
				Shard: []*configpb.LogShardConfig{
					{Uri: hs.URL, NotAfterStart: ts2, NotAfterLimit: nil, PublicKeyDer: p.Bytes},
				},
			},
			wantErr: true,
		},
	}

	ctx := context.Background()
	for _, test := range tests {
		tlc, err := client.NewTemporalLogClient(test.cfg, nil)
		if err != nil {
			t.Errorf("NewTemporalLogClient(%+v)=nil, %v; want _,nil", test.cfg, err)
			continue
		}

		_, err = tlc.AddChain(ctx, certChain)
		if err != nil {
			if !test.wantErr {
				t.Errorf("AddChain()=nil,%v; want sct,nil", err)
			}
		} else if test.wantErr {
			t.Errorf("AddChain()=sct,nil; want nil,_")
		}

		_, err = tlc.AddPreChain(ctx, precertChain)
		if err != nil {
			if !test.wantErr {
				t.Errorf("AddPreChain()=nil,%v; want sct,nil", err)
			}
		} else if test.wantErr {
			t.Errorf("AddPreChain()=sct,nil; want nil,_")
		}
	}
}

func TestTemporalAddChainErrors(t *testing.T) {
	hs := serveSCTAt(t, "/ct/v1/add-chain", testdata.TestCertProof)
	defer hs.Close()

	cfg := &configpb.TemporalLogConfig{
		Shard: []*configpb.LogShardConfig{
			{
				Uri:           hs.URL,
				NotAfterStart: nil,
				NotAfterLimit: nil,
			},
		},
	}

	ctx := context.Background()
	tlc, err := client.NewTemporalLogClient(cfg, nil)
	if err != nil {
		t.Fatalf("NewTemporalLogClient(%+v)=nil, %v; want _,nil", cfg, err)
	}

	_, err = tlc.AddChain(ctx, nil)
	if err == nil {
		t.Errorf("AddChain(nil)=sct,nil; want nil, 'missing chain'")
	}
	_, err = tlc.AddChain(ctx, []ct.ASN1Cert{{Data: []byte{0x01, 0x02}}})
	if err == nil {
		t.Errorf("AddChain(nil)=sct,nil; want nil, 'failed to parse'")
	}

}
