package swarm

import (
	"bytes"
	"io"
	"os"
	"testing"
	"time"

	"github.com/docker/cli/internal/test"
	"github.com/docker/docker/api/types/swarm"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

const (
	cert = `
-----BEGIN CERTIFICATE-----
MIIBuDCCAV4CCQDOqUYOWdqMdjAKBggqhkjOPQQDAzBjMQswCQYDVQQGEwJVUzEL
MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkRv
Y2tlcjEPMA0GA1UECwwGRG9ja2VyMQ0wCwYDVQQDDARUZXN0MCAXDTE4MDcwMjIx
MjkxOFoYDzMwMTcxMTAyMjEyOTE4WjBjMQswCQYDVQQGEwJVUzELMAkGA1UECAwC
Q0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkRvY2tlcjEPMA0G
A1UECwwGRG9ja2VyMQ0wCwYDVQQDDARUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEgvvZl5Vqpr1e+g5IhoU6TZHgRau+BZETVFTmqyWYajA/mooRQ1MZTozu
s9ZZZA8tzUhIqS36gsFuyIZ4YiAlyjAKBggqhkjOPQQDAwNIADBFAiBQ7pCPQrj8
8zaItMf0pk8j1NU5XrFqFEZICzvjzUJQBAIhAKq2gFwoTn8KH+cAAXZpAGJPmOsT
zsBT8gBAOHhNA6/2
-----END CERTIFICATE-----`
	key = `
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICyheZpw70pbgO4hEuwhZTETWyTpNJmJ3TyFaWT6WTRkoAoGCCqGSM49
AwEHoUQDQgAEgvvZl5Vqpr1e+g5IhoU6TZHgRau+BZETVFTmqyWYajA/mooRQ1MZ
Tozus9ZZZA8tzUhIqS36gsFuyIZ4YiAlyg==
-----END EC PRIVATE KEY-----`
)

func swarmSpecWithFullCAConfig() *swarm.Spec {
	return &swarm.Spec{
		CAConfig: swarm.CAConfig{
			SigningCACert:  "cacert",
			SigningCAKey:   "cakey",
			ForceRotate:    1,
			NodeCertExpiry: time.Duration(200),
			ExternalCAs: []*swarm.ExternalCA{
				{
					URL:      "https://example.com/ca",
					Protocol: swarm.ExternalCAProtocolCFSSL,
					CACert:   "excacert",
				},
			},
		},
	}
}

func TestDisplayTrustRootNoRoot(t *testing.T) {
	buffer := new(bytes.Buffer)
	err := displayTrustRoot(buffer, swarm.Swarm{})
	assert.Error(t, err, "No CA information available")
}

type invalidCATestCases struct {
	args     []string
	errorMsg string
}

func writeFile(data string) (string, error) {
	tmpfile, err := os.CreateTemp("", "testfile")
	if err != nil {
		return "", err
	}
	_, err = tmpfile.Write([]byte(data))
	if err != nil {
		return "", err
	}
	return tmpfile.Name(), tmpfile.Close()
}

func TestDisplayTrustRootInvalidFlags(t *testing.T) {
	// we need an actual PEMfile to test
	tmpfile, err := writeFile(cert)
	assert.NilError(t, err)
	t.Cleanup(func() { _ = os.Remove(tmpfile) })

	errorTestCases := []invalidCATestCases{
		{
			args:     []string{"--ca-cert=" + tmpfile},
			errorMsg: "flag requires the `--rotate` flag to update the CA",
		},
		{
			args:     []string{"--ca-key=" + tmpfile},
			errorMsg: "flag requires the `--rotate` flag to update the CA",
		},
		{ // to make sure we're not erroring because we didn't provide a CA key along with the CA cert
			args: []string{
				"--ca-cert=" + tmpfile,
				"--ca-key=" + tmpfile,
			},
			errorMsg: "flag requires the `--rotate` flag to update the CA",
		},
		{
			args:     []string{"--cert-expiry=2160h0m0s"},
			errorMsg: "flag requires the `--rotate` flag to update the CA",
		},
		{
			args:     []string{"--external-ca=protocol=cfssl,url=https://some.example.com/https/url"},
			errorMsg: "flag requires the `--rotate` flag to update the CA",
		},
		{ // to make sure we're not erroring because we didn't provide a CA cert and external CA
			args: []string{
				"--ca-cert=" + tmpfile,
				"--external-ca=protocol=cfssl,url=https://some.example.com/https/url",
			},
			errorMsg: "flag requires the `--rotate` flag to update the CA",
		},
		{
			args: []string{
				"--rotate",
				"--external-ca=protocol=cfssl,url=https://some.example.com/https/url",
			},
			errorMsg: "rotating to an external CA requires the `--ca-cert` flag to specify the external CA's cert - " +
				"to add an external CA with the current root CA certificate, use the `update` command instead",
		},
		{
			args: []string{
				"--rotate",
				"--ca-cert=" + tmpfile,
			},
			errorMsg: "the --ca-cert flag requires that a --ca-key flag and/or --external-ca flag be provided as well",
		},
	}

	for _, testCase := range errorTestCases {
		cmd := newCACommand(
			test.NewFakeCli(&fakeClient{
				swarmInspectFunc: func() (swarm.Swarm, error) {
					return swarm.Swarm{
						ClusterInfo: swarm.ClusterInfo{
							TLSInfo: swarm.TLSInfo{
								TrustRoot: "root",
							},
						},
					}, nil
				},
			}))
		assert.Check(t, cmd.Flags().Parse(testCase.args))
		cmd.SetOut(io.Discard)
		assert.ErrorContains(t, cmd.Execute(), testCase.errorMsg)
	}
}

func TestDisplayTrustRoot(t *testing.T) {
	buffer := new(bytes.Buffer)
	trustRoot := "trustme"
	err := displayTrustRoot(buffer, swarm.Swarm{
		ClusterInfo: swarm.ClusterInfo{
			TLSInfo: swarm.TLSInfo{TrustRoot: trustRoot},
		},
	})
	assert.NilError(t, err)
	assert.Check(t, is.Equal(trustRoot+"\n", buffer.String()))
}

type swarmUpdateRecorder struct {
	spec swarm.Spec
}

func (s *swarmUpdateRecorder) swarmUpdate(sp swarm.Spec, _ swarm.UpdateFlags) error {
	s.spec = sp
	return nil
}

func swarmInspectFuncWithFullCAConfig() (swarm.Swarm, error) {
	return swarm.Swarm{
		ClusterInfo: swarm.ClusterInfo{
			Spec: *swarmSpecWithFullCAConfig(),
		},
	}, nil
}

func TestUpdateSwarmSpecDefaultRotate(t *testing.T) {
	s := &swarmUpdateRecorder{}
	cli := test.NewFakeCli(&fakeClient{
		swarmInspectFunc: swarmInspectFuncWithFullCAConfig,
		swarmUpdateFunc:  s.swarmUpdate,
	})
	cmd := newCACommand(cli)
	cmd.SetArgs([]string{"--rotate", "--detach"})
	cmd.SetOut(cli.OutBuffer())
	assert.NilError(t, cmd.Execute())

	expected := swarmSpecWithFullCAConfig()
	expected.CAConfig.ForceRotate = 2
	expected.CAConfig.SigningCACert = ""
	expected.CAConfig.SigningCAKey = ""
	assert.Check(t, is.DeepEqual(*expected, s.spec))
}

func TestUpdateSwarmSpecCertAndKey(t *testing.T) {
	certfile, err := writeFile(cert)
	assert.NilError(t, err)
	defer os.Remove(certfile)

	keyfile, err := writeFile(key)
	assert.NilError(t, err)
	defer os.Remove(keyfile)

	s := &swarmUpdateRecorder{}
	cli := test.NewFakeCli(&fakeClient{
		swarmInspectFunc: swarmInspectFuncWithFullCAConfig,
		swarmUpdateFunc:  s.swarmUpdate,
	})
	cmd := newCACommand(cli)
	cmd.SetArgs([]string{
		"--rotate",
		"--detach",
		"--ca-cert=" + certfile,
		"--ca-key=" + keyfile,
		"--cert-expiry=3m",
	})
	cmd.SetOut(cli.OutBuffer())
	assert.NilError(t, cmd.Execute())

	expected := swarmSpecWithFullCAConfig()
	expected.CAConfig.SigningCACert = cert
	expected.CAConfig.SigningCAKey = key
	expected.CAConfig.NodeCertExpiry = 3 * time.Minute
	assert.Check(t, is.DeepEqual(*expected, s.spec))
}

func TestUpdateSwarmSpecCertAndExternalCA(t *testing.T) {
	certfile, err := writeFile(cert)
	assert.NilError(t, err)
	defer os.Remove(certfile)

	s := &swarmUpdateRecorder{}
	cli := test.NewFakeCli(&fakeClient{
		swarmInspectFunc: swarmInspectFuncWithFullCAConfig,
		swarmUpdateFunc:  s.swarmUpdate,
	})
	cmd := newCACommand(cli)
	cmd.SetArgs([]string{
		"--rotate",
		"--detach",
		"--ca-cert=" + certfile,
		"--external-ca=protocol=cfssl,url=https://some.external.ca.example.com",
	})
	cmd.SetOut(cli.OutBuffer())
	assert.NilError(t, cmd.Execute())

	expected := swarmSpecWithFullCAConfig()
	expected.CAConfig.SigningCACert = cert
	expected.CAConfig.SigningCAKey = ""
	expected.CAConfig.ExternalCAs = []*swarm.ExternalCA{
		{
			Protocol: swarm.ExternalCAProtocolCFSSL,
			URL:      "https://some.external.ca.example.com",
			CACert:   cert,
			Options:  make(map[string]string),
		},
	}
	assert.Check(t, is.DeepEqual(*expected, s.spec))
}

func TestUpdateSwarmSpecCertAndKeyAndExternalCA(t *testing.T) {
	certfile, err := writeFile(cert)
	assert.NilError(t, err)
	defer os.Remove(certfile)

	keyfile, err := writeFile(key)
	assert.NilError(t, err)
	defer os.Remove(keyfile)

	s := &swarmUpdateRecorder{}
	cli := test.NewFakeCli(&fakeClient{
		swarmInspectFunc: swarmInspectFuncWithFullCAConfig,
		swarmUpdateFunc:  s.swarmUpdate,
	})
	cmd := newCACommand(cli)
	cmd.SetArgs([]string{
		"--rotate",
		"--detach",
		"--ca-cert=" + certfile,
		"--ca-key=" + keyfile,
		"--external-ca=protocol=cfssl,url=https://some.external.ca.example.com",
	})
	cmd.SetOut(cli.OutBuffer())
	assert.NilError(t, cmd.Execute())

	expected := swarmSpecWithFullCAConfig()
	expected.CAConfig.SigningCACert = cert
	expected.CAConfig.SigningCAKey = key
	expected.CAConfig.ExternalCAs = []*swarm.ExternalCA{
		{
			Protocol: swarm.ExternalCAProtocolCFSSL,
			URL:      "https://some.external.ca.example.com",
			CACert:   cert,
			Options:  make(map[string]string),
		},
	}
	assert.Check(t, is.DeepEqual(*expected, s.spec))
}
