// Copyright Istio 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 util

import (
	"crypto/x509"
	"log"
	"os"
	"strings"
	"testing"
	"time"
)

func loadPEMFile(path string) string {
	b, err := os.ReadFile(path)
	if err != nil {
		log.Fatalf("failed to load the pem file = %v, err = %v", path, err)
	}
	return string(b)
}

var (
	key             = loadPEMFile("../testdata/key-10y.pem")
	keyMismatch     = loadPEMFile("../testdata/key-mismatch.pem")
	keyBad          = loadPEMFile("../testdata/key-verify-fail.pem")
	certChainBad    = loadPEMFile("../testdata/cert-verify-fail.pem")
	certChainNoRoot = loadPEMFile("../testdata/cert-noroot.pem")
	certChain       = loadPEMFile("../testdata/cert-chain-10y.pem")
	rootCertBad     = loadPEMFile("../testdata/root-verify-fail.pem")
	rootCert        = loadPEMFile("../testdata/root-cert-10y.pem")
	verifyField1    = &VerifyFields{
		Host: "",
	}

	verifyField2 = &VerifyFields{
		Host: "spiffe",
	}

	notBefore = &VerifyFields{
		NotBefore: time.Unix(0, 0),
		Host:      "spiffe://cluster.local/ns/default/sa/default",
	}

	ttl = &VerifyFields{
		TTL:  time.Duration(0),
		Host: "spiffe://cluster.local/ns/default/sa/default",
	}

	extKeyUsage = &VerifyFields{
		TTL:  time.Duration(1),
		Host: "spiffe://cluster.local/ns/default/sa/default",
	}

	keyUsage = &VerifyFields{
		ExtKeyUsage: []x509.ExtKeyUsage{1, 2},
		KeyUsage:    2,
		Host:        "spiffe://cluster.local/ns/default/sa/default",
	}

	isCA = &VerifyFields{
		ExtKeyUsage: []x509.ExtKeyUsage{1, 2},
		KeyUsage:    5,
		IsCA:        true,
		Host:        "spiffe://cluster.local/ns/default/sa/default",
	}

	org = &VerifyFields{
		ExtKeyUsage: []x509.ExtKeyUsage{1, 2},
		KeyUsage:    5,
		Org:         "bad",
		Host:        "spiffe://cluster.local/ns/default/sa/default",
	}

	success = &VerifyFields{
		ExtKeyUsage: []x509.ExtKeyUsage{1, 2},
		KeyUsage:    5,
		Host:        "spiffe://cluster.local/ns/default/sa/default",
	}
)

func TestVerifyCert(t *testing.T) {
	testCases := map[string]struct {
		privPem        []byte
		certChainPem   []byte
		rootCertPem    []byte
		expectedFields *VerifyFields
		expectedErr    string
	}{
		"Root cert bad": {
			privPem:        nil,
			certChainPem:   nil,
			rootCertPem:    []byte(rootCertBad),
			expectedFields: verifyField1,
			expectedErr:    "failed to parse root certificate",
		},
		"Cert chain bad": {
			privPem:        nil,
			certChainPem:   []byte(certChainBad),
			rootCertPem:    []byte(rootCert),
			expectedFields: verifyField1,
			expectedErr:    "failed to parse certificate chain",
		},
		"Failed to verify cert chain": {
			privPem:        nil,
			certChainPem:   []byte(certChainNoRoot),
			rootCertPem:    []byte(rootCert),
			expectedFields: verifyField2,
			expectedErr:    "failed to verify certificate: x509:",
		},
		"Failed to verify key": {
			privPem:        []byte(keyBad),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: verifyField2,
			expectedErr:    "invalid PEM-encoded key",
		},
		"Failed to match key/cert": {
			privPem:        []byte(keyMismatch),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: verifyField2,
			expectedErr:    "the generated private RSA key and cert doesn't match",
		},
		"Wrong SAN": {
			privPem:        []byte(key),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: verifyField2,
			expectedErr:    "the certificate doesn't have the expected SAN for: spiffe",
		},
		"Timestamp error": {
			privPem:        []byte(key),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: notBefore,
			expectedErr:    "unexpected value for 'NotBefore' field",
		},
		"TTL error": {
			privPem:        []byte(key),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: extKeyUsage,
			expectedErr:    "unexpected value for 'NotAfter' - 'NotBefore'",
		},
		"extKeyUsage error": {
			privPem:        []byte(key),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: ttl,
			expectedErr:    "unexpected value for 'ExtKeyUsage' field",
		},
		"KeyUsage Error": {
			privPem:        []byte(key),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: keyUsage,
			expectedErr:    "unexpected value for 'KeyUsage' field",
		},
		"IsCA error": {
			privPem:        []byte(key),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: isCA,
			expectedErr:    "unexpected value for 'IsCA' field",
		},
		"Org error": {
			privPem:        []byte(key),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: org,
			expectedErr:    "unexpected value for 'Organization' field",
		},
		"Succeeded": {
			privPem:        []byte(key),
			certChainPem:   []byte(certChain),
			rootCertPem:    []byte(rootCert),
			expectedFields: success,
			expectedErr:    "",
		},
	}
	for id, tc := range testCases {
		err := VerifyCertificate(
			tc.privPem, tc.certChainPem, tc.rootCertPem, tc.expectedFields)
		if err != nil {
			if len(tc.expectedErr) == 0 {
				t.Errorf("%s: Unexpected error: %v", id, err)
			} else if !strings.Contains(err.Error(), tc.expectedErr) {
				t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr)
			}
		} else if len(tc.expectedErr) != 0 {
			t.Errorf("%s: Expected error %s but succeeded", id, tc.expectedErr)
		}
	}
}

func TestCertExpired(t *testing.T) {
	testCases := map[string]struct {
		filepath string
		expected bool
	}{
		"Expired Cert": {
			filepath: "../testdata/expired-cert.pem",
			expected: true,
		},
		"Not Expired Cert": {
			filepath: "../testdata/notexpired-cert.pem",
			expected: false,
		},
	}
	for id, tc := range testCases {
		t.Run(id, func(t *testing.T) {
			certExpired, err := IsCertExpired(tc.filepath)
			if err != nil {
				t.Fatalf("failed to check the cert, err is: %v", err)
			}
			if certExpired != tc.expected {
				t.Errorf("isCertExpired: get %v, want %v", certExpired, tc.expected)
			}
		})
	}
}
