/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.

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.

SPDX-License-Identifier: Apache-2.0
*/

package configfile

import (
	"os"
	"path/filepath"

	"github.com/cloudnative-pg/machinery/pkg/fileutils"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("update Postgres configuration files", func() {
	var tmpDir string

	_ = BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "configuration-test-")
		Expect(err).NotTo(HaveOccurred())
	})

	_ = AfterEach(func() {
		Expect(os.RemoveAll(tmpDir)).To(Succeed())
	})

	It("must append missing keys", func() {
		initialContent := "# Do not edit this file manually!\n" +
			"# It will be overwritten by the ALTER SYSTEM command.\n" +
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeNameBis'\n" +
			"\n" +
			"recovery_target_timeline = 'latest'\n"

		testFile := filepath.Join(tmpDir, "custom.conf")
		Expect(fileutils.WriteStringToFile(testFile, initialContent)).To(BeTrue())

		Expect(UpdatePostgresConfigurationFile(testFile, map[string]string{
			"test.key":         "test.value",
			"primary_conninfo": "host=someUpdatedHost user=someUpdatedUser application_name=nodeNameBis",
		})).To(BeTrue())

		wantedContent := "# Do not edit this file manually!\n" +
			"# It will be overwritten by the ALTER SYSTEM command.\n" +
			"primary_conninfo = 'host=someUpdatedHost user=someUpdatedUser application_name=nodeNameBis'\n" +
			"\n" +
			"recovery_target_timeline = 'latest'\n" +
			"test.key = 'test.value'\n"

		finalContent, err := fileutils.ReadFile(testFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(finalContent)).To(Equal(wantedContent))
	})

	It("must remove missing managed keys", func() {
		initialContent := "# Do not edit this file manually!\n" +
			"# It will be overwritten by the ALTER SYSTEM command.\n" +
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeNameBis'\n" +
			"\n" +
			"recovery_target_timeline = 'latest'\n"

		testFile := filepath.Join(tmpDir, "custom.conf")
		Expect(fileutils.WriteStringToFile(testFile, initialContent)).To(BeTrue())

		Expect(UpdatePostgresConfigurationFile(testFile, map[string]string{
			"test.key":         "test.value",
			"primary_conninfo": "host=someUpdatedHost user=someUpdatedUser application_name=nodeNameBis",
		}, "recovery_target_timeline")).To(BeTrue())

		wantedContent := "# Do not edit this file manually!\n" +
			"# It will be overwritten by the ALTER SYSTEM command.\n" +
			"primary_conninfo = 'host=someUpdatedHost user=someUpdatedUser application_name=nodeNameBis'\n" +
			"\n" +
			"test.key = 'test.value'\n"

		finalContent, err := fileutils.ReadFile(testFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(finalContent)).To(Equal(wantedContent))
	})

	It("must not change managed keys when present", func() {
		initialContent := "# Do not edit this file manually!\n" +
			"# It will be overwritten by the ALTER SYSTEM command.\n" +
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeNameBis'\n" +
			"\n" +
			"recovery_target_timeline = 'latest'\n"

		testFile := filepath.Join(tmpDir, "custom.conf")
		Expect(fileutils.WriteStringToFile(testFile, initialContent)).To(BeTrue())

		Expect(UpdatePostgresConfigurationFile(testFile, map[string]string{
			"test.key":         "test.value",
			"primary_conninfo": "host=someUpdatedHost user=someUpdatedUser application_name=nodeNameBis",
		}, "primary_conninfo")).To(BeTrue())

		wantedContent := "# Do not edit this file manually!\n" +
			"# It will be overwritten by the ALTER SYSTEM command.\n" +
			"primary_conninfo = 'host=someUpdatedHost user=someUpdatedUser application_name=nodeNameBis'\n" +
			"\n" +
			"recovery_target_timeline = 'latest'\n" +
			"test.key = 'test.value'\n"

		finalContent, err := fileutils.ReadFile(testFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(finalContent)).To(Equal(wantedContent))
	})

	It("must work with missing files", func() {
		testFile := filepath.Join(tmpDir, "custom.conf")
		Expect(fileutils.FileExists(testFile)).To(BeFalse())

		Expect(UpdatePostgresConfigurationFile(testFile, map[string]string{
			"primary_conninfo": "host=someHost user=someUser application_name=nodeName",
		})).To(BeTrue())

		wantedContent := "primary_conninfo = 'host=someHost user=someUser application_name=nodeName'\n"

		finalContent, err := fileutils.ReadFile(testFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(finalContent)).To(Equal(wantedContent))
	})

	It("must add missing includes", func() {
		initialContent := "# This is a test file!\n" +
			"# Comments are ignored\n" +
			"\n" +
			"one_key = 'one value'\n" +
			"another_key = On"

		testFile := filepath.Join(tmpDir, "postgresql.conf")
		Expect(fileutils.WriteStringToFile(testFile, initialContent)).To(BeTrue())

		Expect(EnsureIncludes(testFile, "custom.conf", "override.conf")).To(BeTrue())

		wantedContent := "# This is a test file!\n" +
			"# Comments are ignored\n" +
			"\n" +
			"one_key = 'one value'\n" +
			"another_key = On\n" +
			"\n" +
			"# load CloudNativePG custom.conf configuration\n" +
			"include 'custom.conf'\n" +
			"\n" +
			"# load CloudNativePG override.conf configuration\n" +
			"include 'override.conf'\n"

		finalContent, err := fileutils.ReadFile(testFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(finalContent)).To(Equal(wantedContent))
	})

	It("must left untouched existing includes", func() {
		initialContent := "# This is a test file!\n" +
			"# Comments are ignored\n" +
			"one_key = 'one value'\n" +
			"another_key = On\n" +
			"\n" +
			"# customized comment\n" +
			"include 'custom.conf'"

		testFile := filepath.Join(tmpDir, "postgresql.conf")
		Expect(fileutils.WriteStringToFile(testFile, initialContent)).To(BeTrue())

		Expect(EnsureIncludes(testFile, "custom.conf", "override.conf")).To(BeTrue())

		wantedContent := "# This is a test file!\n" +
			"# Comments are ignored\n" +
			"one_key = 'one value'\n" +
			"another_key = On\n" +
			"\n" +
			"# customized comment\n" +
			"include 'custom.conf'\n" +
			"\n" +
			"# load CloudNativePG override.conf configuration\n" +
			"include 'override.conf'\n"

		finalContent, err := fileutils.ReadFile(testFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(finalContent)).To(Equal(wantedContent))
	})
})

var _ = Describe("Update configuration files", func() {
	It("must append missing keys", func() {
		initialContent := []string{
			"# Do not edit this file manually!",
			"# It will be overwritten by the ALTER SYSTEM command.",
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeName'",
			"",
			"recovery_target_timeline = 'latest'",
		}

		updatedContent, _ := UpdateConfigurationContents(initialContent, map[string]string{
			"test.key": "test.value",
		})

		wantedContent := []string{
			"# Do not edit this file manually!",
			"# It will be overwritten by the ALTER SYSTEM command.",
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeName'",
			"",
			"recovery_target_timeline = 'latest'",
			"test.key = 'test.value'",
		}

		Expect(updatedContent).To(Equal(wantedContent))
	})

	It("must remove repeated keys", func() {
		initialContent := []string{
			"# Do not edit this file manually!",
			"# It will be overwritten by the ALTER SYSTEM command.",
			"primary_conninfo = 'host=someHost1 user=someUser1 application_name=nodeName1'",
			"",
			"recovery_target_timeline = 'latest'",
			"primary_conninfo = 'host=someHost2 user=someUser2 application_name=nodeName2'",
		}

		updatedContent, _ := UpdateConfigurationContents(initialContent, map[string]string{
			"primary_conninfo": "host=someHost user=someUser application_name=nodeName",
		})

		wantedContent := []string{
			"# Do not edit this file manually!",
			"# It will be overwritten by the ALTER SYSTEM command.",
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeName'",
			"",
			"recovery_target_timeline = 'latest'",
		}

		Expect(updatedContent).To(Equal(wantedContent))
	})
})

var _ = Describe("Remove configuration files option", func() {
	It("keeps the initial input if the option to be removed is not matched", func() {
		initialContent := []string{
			"# Do not edit this file manually!",
			"# It will be overwritten by the ALTER SYSTEM command.",
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeName'",
			"",
			"recovery_target_timeline = 'latest'",
		}

		updatedContent := RemoveOptionsFromConfigurationContents(initialContent, "archive_mode")

		Expect(updatedContent).To(Equal(initialContent))
	})

	It("must delete lines with the given option", func() {
		initialContent := []string{
			"# Do not edit this file manually!",
			"# It will be overwritten by the ALTER SYSTEM command.",
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeName'",
			"",
			"archive_mode = 'on'",
			"recovery_target_timeline = 'latest'",
			"archive_mode = 'always'",
		}

		updatedContent := RemoveOptionsFromConfigurationContents(initialContent, "archive_mode")

		wantedContent := []string{
			"# Do not edit this file manually!",
			"# It will be overwritten by the ALTER SYSTEM command.",
			"primary_conninfo = 'host=someHost user=someUser application_name=nodeName'",
			"",
			"recovery_target_timeline = 'latest'",
		}

		Expect(updatedContent).To(Equal(wantedContent))
	})
})
