package swarm

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

	"github.com/docker/cli/internal/test"
	"github.com/moby/moby/api/types/swarm"
	"github.com/moby/moby/client"
	"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, client.SwarmInspectResult{})
	assert.Error(t, err, "no CA information available")
}

type invalidCATestCases struct {
	args     []string
	errorMsg string
}

func writeFile(dir, data string) (string, error) {
	tmpFile, err := os.CreateTemp(dir, "testfile")
	if err != nil {
		return "", err
	}
	_, err = tmpFile.WriteString(data)
	if err != nil {
		return "", err
	}
	return tmpFile.Name(), tmpFile.Close()
}

func TestDisplayTrustRootInvalidFlags(t *testing.T) {
	// we need an actual PEMfile to test
	tmpDir := t.TempDir()
	tmpFile, err := writeFile(tmpDir, cert)
	assert.NilError(t, err)

	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() (client.SwarmInspectResult, error) {
					return client.SwarmInspectResult{
						Swarm: swarm.Swarm{
							ClusterInfo: swarm.ClusterInfo{
								TLSInfo: swarm.TLSInfo{
									TrustRoot: "root",
								},
							},
						},
					}, nil
				},
			}))
		cmd.SetArgs([]string{})
		cmd.SetOut(io.Discard)
		cmd.SetErr(io.Discard)
		assert.Check(t, cmd.Flags().Parse(testCase.args))
		assert.ErrorContains(t, cmd.Execute(), testCase.errorMsg)
	}
}

func TestDisplayTrustRoot(t *testing.T) {
	buffer := new(bytes.Buffer)
	trustRoot := "trustme"
	err := displayTrustRoot(buffer, client.SwarmInspectResult{
		Swarm: 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(opts client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) {
	s.spec = opts.Spec
	return client.SwarmUpdateResult{}, nil
}

func swarmInspectFuncWithFullCAConfig() (client.SwarmInspectResult, error) {
	return client.SwarmInspectResult{
		Swarm: 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) {
	tmpDir := t.TempDir()
	certFile, err := writeFile(tmpDir, cert)
	assert.NilError(t, err)

	keyFile, err := writeFile(tmpDir, key)
	assert.NilError(t, err)

	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) {
	tmpDir := t.TempDir()
	certFile, err := writeFile(tmpDir, cert)
	assert.NilError(t, err)

	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) {
	tmpDir := t.TempDir()
	certFile, err := writeFile(tmpDir, cert)
	assert.NilError(t, err)

	keyFile, err := writeFile(tmpDir, key)
	assert.NilError(t, err)

	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))
}
