package pki

import (
	"bytes"
	"crypto/sha256"
	"crypto/x509"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"os"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/assert"

	"go.step.sm/crypto/jose"
	"go.step.sm/linkedca"

	"github.com/smallstep/certificates/cas/apiv1"
)

func TestPKI_WriteHelmTemplate(t *testing.T) {
	var preparePKI = func(t *testing.T, opts ...Option) *PKI {
		o := apiv1.Options{
			Type:      "softcas",
			IsCreator: true,
		}

		// Add default WithHelm option
		opts = append(opts, WithHelm())

		// TODO(hs): invoking `New` doesn't perform all operations that are executed
		// when `ca init --helm` is executed. Ideally this logic should be handled
		// in one place and probably inside of the PKI initialization. For testing
		// purposes the missing operations to fill a Helm template fully are faked
		// by `setKeyPair`, `setCertificates` and `setSSHSigningKeys`
		p, err := New(o, opts...)
		assert.NoError(t, err)

		// setKeyPair sets a predefined JWK and a default JWK provisioner. This is one
		// of the things performed in the `ca init` code that's not part of `New`, but
		// performed after that in p.GenerateKeyPairs`. We're currently using the same
		// JWK for every test to keep test variance small: we're not testing JWK generation
		// here after all. It's a bit dangerous to redefine the function here, but it's
		// the simplest way to make this fully testable without refactoring the init now.
		// The password for the predefined encrypted key is \x01\x03\x03\x07.
		setKeyPair(t, p)

		// setCertificates sets some static intermediate and root CA certificate bytes. It
		// replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`,
		// and `p.GenerateIntermediateCertificate`.
		setCertificates(t, p)

		// setSSHSigningKeys sets predefined SSH user and host certificate and key bytes.
		// This replaces the logic in `p.GenerateSSHSigningKeys`
		setSSHSigningKeys(t, p)

		return p
	}
	type test struct {
		pki      *PKI
		testFile string
		wantErr  bool
	}
	var tests = map[string]func(t *testing.T) test{
		"ok/simple": func(t *testing.T) test {
			return test{
				pki:      preparePKI(t),
				testFile: "testdata/helm/simple.yml",
				wantErr:  false,
			}
		},
		"ok/with-provisioner": func(t *testing.T) test {
			return test{
				pki:      preparePKI(t, WithProvisioner("a-provisioner")),
				testFile: "testdata/helm/with-provisioner.yml",
				wantErr:  false,
			}
		},
		"ok/with-acme": func(t *testing.T) test {
			return test{
				pki:      preparePKI(t, WithACME()),
				testFile: "testdata/helm/with-acme.yml",
				wantErr:  false,
			}
		},
		"ok/with-acme-and-duplicate-provisioner-name": func(t *testing.T) test {
			return test{
				pki:      preparePKI(t, WithProvisioner("acme"), WithACME()),
				testFile: "testdata/helm/with-acme-and-duplicate-provisioner-name.yml",
				wantErr:  false,
			}
		},
		"ok/with-admin": func(t *testing.T) test {
			return test{
				pki:      preparePKI(t, WithAdmin()),
				testFile: "testdata/helm/with-admin.yml",
				wantErr:  false,
			}
		},
		"ok/with-ssh": func(t *testing.T) test {
			return test{
				pki:      preparePKI(t, WithSSH()),
				testFile: "testdata/helm/with-ssh.yml",
				wantErr:  false,
			}
		},
		"ok/with-ssh-and-duplicate-provisioner-name": func(t *testing.T) test {
			return test{
				pki:      preparePKI(t, WithProvisioner("sshpop"), WithSSH()),
				testFile: "testdata/helm/with-ssh-and-duplicate-provisioner-name.yml",
				wantErr:  false,
			}
		},
		"ok/with-ssh-and-acme": func(t *testing.T) test {
			return test{
				pki:      preparePKI(t, WithSSH(), WithACME()),
				testFile: "testdata/helm/with-ssh-and-acme.yml",
				wantErr:  false,
			}
		},
		"fail/authority.ProvisionerToCertificates": func(t *testing.T) test {
			pki := preparePKI(t)
			pki.Authority.Provisioners = append(pki.Authority.Provisioners,
				&linkedca.Provisioner{
					Type:    linkedca.Provisioner_JWK,
					Name:    "Broken JWK",
					Details: nil,
				},
			)
			return test{
				pki:     pki,
				wantErr: true,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {

			w := &bytes.Buffer{}
			if err := tc.pki.WriteHelmTemplate(w); (err != nil) != tc.wantErr {
				t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tc.wantErr)
				return
			}

			if tc.wantErr {
				// don't compare output if an error was expected on output
				return
			}

			wantBytes, err := os.ReadFile(tc.testFile)
			assert.NoError(t, err)
			if diff := cmp.Diff(wantBytes, w.Bytes()); diff != "" {
				t.Logf("Generated Helm template did not match reference %q\n", tc.testFile)
				t.Errorf("Diff follows:\n%s\n", diff)
				t.Errorf("Full output:\n%s\n", w.Bytes())
			}
		})
	}
}

// setKeyPair sets a predefined JWK and a default JWK provisioner.
func setKeyPair(t *testing.T, p *PKI) {
	t.Helper()

	var err error

	p.ottPublicKey, err = jose.ParseKey([]byte(`{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"}`))
	if err != nil {
		t.Fatal(err)
	}

	p.ottPrivateKey, err = jose.ParseEncrypted("eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA")
	if err != nil {
		t.Fatal(err)
	}

	var claims *linkedca.Claims
	if p.options.enableSSH {
		claims = &linkedca.Claims{
			Ssh: &linkedca.SSHClaims{
				Enabled: true,
			},
		}
	}

	publicKey, err := json.Marshal(p.ottPublicKey)
	if err != nil {
		t.Fatal(err)
	}
	encryptedKey, err := p.ottPrivateKey.CompactSerialize()
	if err != nil {
		t.Fatal(err)
	}
	p.Authority.Provisioners = append(p.Authority.Provisioners, &linkedca.Provisioner{
		Type:   linkedca.Provisioner_JWK,
		Name:   p.options.provisioner,
		Claims: claims,
		Details: &linkedca.ProvisionerDetails{
			Data: &linkedca.ProvisionerDetails_JWK{
				JWK: &linkedca.JWKProvisioner{
					PublicKey:           publicKey,
					EncryptedPrivateKey: []byte(encryptedKey),
				},
			},
		},
	})
}

// setCertificates sets some static, gibberish intermediate and root CA certificate and key bytes.
func setCertificates(_ *testing.T, p *PKI) {
	raw := []byte("these are just some fake root CA cert bytes")
	p.Files[p.Root[0]] = encodeCertificate(&x509.Certificate{Raw: raw})
	p.Files[p.RootKey[0]] = pem.EncodeToMemory(&pem.Block{
		Type:  "EC PRIVATE KEY",
		Bytes: []byte("these are just some fake root CA key bytes"),
	})
	p.Files[p.Intermediate] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake intermediate CA cert bytes")})
	p.Files[p.IntermediateKey] = pem.EncodeToMemory(&pem.Block{
		Type:  "EC PRIVATE KEY",
		Bytes: []byte("these are just some fake intermediate CA key bytes"),
	})
	sum := sha256.Sum256(raw)
	p.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:]))
}

// setSSHSigningKeys sets some static, gibberish ssh user and host CA certificate and key bytes.
func setSSHSigningKeys(_ *testing.T, p *PKI) {
	if !p.options.enableSSH {
		return
	}

	p.Files[p.Ssh.HostKey] = pem.EncodeToMemory(&pem.Block{
		Type:  "EC PRIVATE KEY",
		Bytes: []byte("fake ssh host key bytes"),
	})
	p.Files[p.Ssh.HostPublicKey] = []byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms=")
	p.Files[p.Ssh.UserKey] = pem.EncodeToMemory(&pem.Block{
		Type:  "EC PRIVATE KEY",
		Bytes: []byte("fake ssh user key bytes"),
	})
	p.Files[p.Ssh.UserPublicKey] = []byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs=")
}
