package integration

import (
	"fmt"
	"sort"

	"github.com/apache/cloudberry-backup/backup"
	"github.com/apache/cloudberry-backup/options"
	"github.com/apache/cloudberry-go-libs/testhelper"

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

func createGenericPartitionTable(tableName string) {
	testhelper.AssertQueryRuns(connectionPool, fmt.Sprintf(`CREATE TABLE %s (id int, gender char(1))
		DISTRIBUTED BY (id)
		PARTITION BY LIST (gender)
		( PARTITION girls VALUES ('F'),
		  PARTITION boys VALUES ('M'),
		  DEFAULT PARTITION other )`, tableName))
}

func createPartitionTableWithExternalPartition(tableName string, extPartName string) {
	createGenericPartitionTable(tableName)
	testhelper.AssertQueryRuns(connectionPool, fmt.Sprintf(`CREATE EXTERNAL WEB TABLE %[1]s_ext_part_ (like %[1]s_1_prt_%[2]s)
EXECUTE 'echo -e "2\n1"' on host
FORMAT 'csv';`, tableName, extPartName))
	testhelper.AssertQueryRuns(connectionPool, fmt.Sprintf(`ALTER TABLE %[1]s EXCHANGE PARTITION %[2]s WITH TABLE %[1]s_ext_part_ WITHOUT VALIDATION;`, tableName, extPartName))
}

func dropTableWithExternalPartition(tableName string) {
	testhelper.AssertQueryRuns(connectionPool, fmt.Sprintf("DROP TABLE %s", tableName))
	testhelper.AssertQueryRuns(connectionPool, fmt.Sprintf("DROP TABLE %s_ext_part_", tableName))
}

var _ = Describe("Options Integration", func() {
	Describe("QuoteTablesNames", func() {
		It("quotes identifiers as expected", func() {
			tableList := []string{
				`foo.bar`,  // no special characters
				`foo.BAR`,  // capital characters
				`foo.'bar`, // make sure that single quotes are escaped before string is fed to quote_ident
				`foo.2`,    // numbers
				`foo._bar`, // underscore
				`foo ~#$%^&*()_-+[]{}><\|;:/?!,.bar`,
				"foo.\tbar", // important to use double quotes to allow \t to become tab
				"foo.\nbar", // important to use double quotes to allow \n to become a new line
				`foo.\n`,
				`foo."bar`, // quote ident should escape double-quote with another double-quote
			}
			expected := []string{
				`foo.bar`,
				`foo."BAR"`,
				`foo."'bar"`,
				`foo."2"`,
				`foo._bar`, // underscore is not a special character
				`"foo ~#$%^&*()_-+[]{}><\|;:/?!,".bar`,
				"foo.\"\tbar\"",
				"foo.\"\nbar\"",
				`foo."\n"`,
				`foo."""bar"`,
			}

			resultFQNs, err := options.QuoteTableNames(connectionPool, tableList)
			Expect(err).ToNot(HaveOccurred())
			Expect(expected).To(Equal(resultFQNs))
		})
	})
	Describe("ValidateFilterTables", func() {
		It("validates special chars", func() {
			createSpecialCharacterTables := `
-- special chars
CREATE TABLE public."FOObar" (i int);
CREATE TABLE public."BAR" (i int);
CREATE SCHEMA "CAPschema";
CREATE TABLE "CAPschema"."BAR" (i int);
CREATE TABLE "CAPschema".baz (i int);
CREATE TABLE public.foo_bar (i int);
CREATE TABLE public."foo ~#$%^.&*()_-+[]{}><\|;:/?!bar" (i int);
CREATE SCHEMA "dot.schema";
CREATE TABLE "dot.schema"."dot.table" (i int);
-- special chars: embedded tab char
CREATE TABLE public."tab	bar" (i int);
-- special chars: embedded newline char
CREATE TABLE public."newline
bar" (i int);
`
			dropSpecialCharacterTables := `
-- special chars
DROP TABLE public."FOObar";
DROP TABLE public."BAR";
DROP SCHEMA "CAPschema" cascade;
DROP TABLE public.foo_bar;
DROP TABLE public."foo ~#$%^.&*()_-+[]{}><\|;:/?!bar";
DROP SCHEMA "dot.schema" cascade;
-- special chars: embedded tab char
DROP TABLE public."tab	bar";
-- special chars: embedded newline char
DROP TABLE public."newline
bar";
`
			testhelper.AssertQueryRuns(connectionPool, createSpecialCharacterTables)
			defer testhelper.AssertQueryRuns(connectionPool, dropSpecialCharacterTables)

			tableList := []string{
				`public."BAR"`,
				`"CAPschema"."BAR"`,
				`"CAPschema".baz`,
				`public.foo_bar`,
				`public."foo ~#$%^.&*()_-+[]{}><\|;:/?!bar"`,
				`"dot.schema"."dot.table"`,
				"public.\"tab\tbar\"",     // important to use double quotes to allow \t to become tab
				"public.\"newline\nbar\"", // important to use double quotes to allow \n to become newline
			}

			backup.ValidateTablesExist(connectionPool, tableList, false)
		})
	})
	Describe("ExpandIncludesForPartitions", func() {
		BeforeEach(func() {
			createGenericPartitionTable(`public."CAPpart"`)
		})

		AfterEach(func() {
			testhelper.AssertQueryRuns(connectionPool, `DROP TABLE public."CAPpart"`)
		})
		It("adds parent table when child partition with special chars is included", func() {
			parentTable := `public.CAPpart`
			partTable := `public.CAPpart_1_prt_girls`

			err := backupCmdFlags.Set(options.INCLUDE_RELATION, partTable)
			Expect(err).ToNot(HaveOccurred())
			_ = backupCmdFlags.Set(options.LEAF_PARTITION_DATA, "true")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			Expect(subject.GetIncludedTables()).To(ContainElement(partTable))
			Expect(subject.GetIncludedTables()).To(HaveLen(1))

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			Expect(subject.GetIncludedTables()).To(HaveLen(2))
			Expect(backupCmdFlags.GetStringArray(options.INCLUDE_RELATION)).To(HaveLen(2))
			Expect(subject.GetIncludedTables()).To(ContainElement(partTable))
			Expect(subject.GetIncludedTables()).To(ContainElement(parentTable))

			// ensure that ExpandIncludesForPartitions does not disturb the original value
			// that the user typed in, which is used by initializeBackupReport() and
			// is important for incremental backups which must exactly match all flag input
			Expect(subject.GetOriginalIncludedTables()).To(HaveLen(1))
			Expect(subject.GetOriginalIncludedTables()).To(Equal([]string{partTable}))
		})
		It("adds parent table when child partition with embedded quote character is included", func() {
			createGenericPartitionTable(`public."""hasquote"""`)
			defer testhelper.AssertQueryRuns(connectionPool, `DROP TABLE public."""hasquote"""`)

			parentTable := `public."hasquote"`
			partTable := `public."hasquote"_1_prt_girls`
			err := backupCmdFlags.Set(options.INCLUDE_RELATION, partTable)
			_ = backupCmdFlags.Set(options.LEAF_PARTITION_DATA, "true")
			Expect(err).ToNot(HaveOccurred())
			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			Expect(subject.GetIncludedTables()).To(ContainElement(partTable))
			Expect(subject.GetIncludedTables()).To(HaveLen(1))

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			Expect(subject.GetIncludedTables()).To(HaveLen(2))
			Expect(backupCmdFlags.GetStringArray(options.INCLUDE_RELATION)).To(HaveLen(2))
			Expect(subject.GetIncludedTables()[0]).To(Equal(partTable))
			Expect(subject.GetIncludedTables()[1]).To(Equal(parentTable))
		})
		It("returns child partition tables for an included parent table if the leaf-partition-data flag is set and the filter includes a parent partition table", func() {
			_ = backupCmdFlags.Set(options.LEAF_PARTITION_DATA, "true")
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.rank")

			createGenericPartitionTable(`public.rank`)
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.rank")
			testhelper.AssertQueryRuns(connectionPool, "CREATE TABLE public.test_table(i int)")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.test_table")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			expectedTableNames := []string{
				"public.rank",
				"public.rank_1_prt_boys",
				"public.rank_1_prt_girls",
				"public.rank_1_prt_other",
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
		It("returns parent and external leaf partition table if the filter includes a leaf table and leaf-partition-data is set", func() {
			_ = backupCmdFlags.Set(options.LEAF_PARTITION_DATA, "true")
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.partition_table_1_prt_boys")
			createPartitionTableWithExternalPartition("public.partition_table", "girls")
			defer dropTableWithExternalPartition("public.partition_table")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			expectedTableNames := []string{
				"public.partition_table",
				"public.partition_table_1_prt_boys",
			}
			if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
				// For GPDB 4, 5, and 6, the leaf partition metadata is a part of the large root partition DDL.
				// For GPDB 7+, the leaf partition has its own separate DDL and attaches onto the root partition
				// with ALTER TABLE ATTACH PARTITION. Therefore, only the included leaf partition and its root
				// partition will have their metadata dumped.
				expectedTableNames = append(expectedTableNames, "public.partition_table_1_prt_girls")
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
		It("returns the parent table and no unrelated external partitions if the filter includes a leaf table and leaf-partition-data and no-inherits are set", func() {
			_ = backupCmdFlags.Set(options.LEAF_PARTITION_DATA, "true")
			_ = backupCmdFlags.Set(options.NO_INHERITS, "true")
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.partition_table_1_prt_boys")
			createPartitionTableWithExternalPartition("public.partition_table", "girls")
			defer dropTableWithExternalPartition("public.partition_table")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			expectedTableNames := []string{
				"public.partition_table_1_prt_boys",
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
		It("returns external partition tables for an included parent table if the filter includes a parent partition table", func() {
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.partition_table1")
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.partition_table2_1_prt_other")

			// must include leaf partition flag, as it is required to validate the include lists and populate the global structures
			// this results in a much broader partition inclusion on GPDB6 and before
			_ = backupCmdFlags.Set(options.LEAF_PARTITION_DATA, "true")

			createPartitionTableWithExternalPartition("public.partition_table1", "boys")
			defer dropTableWithExternalPartition("public.partition_table1")
			createPartitionTableWithExternalPartition("public.partition_table2", "girls")
			defer dropTableWithExternalPartition("public.partition_table2")
			createPartitionTableWithExternalPartition("public.partition_table3", "girls")
			defer dropTableWithExternalPartition("public.partition_table3")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			var expectedTableNames []string
			if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
				expectedTableNames = []string{
					"public.partition_table1",
					"public.partition_table1_1_prt_boys",
					"public.partition_table1_1_prt_girls",
					"public.partition_table1_1_prt_other",
					"public.partition_table2",
					"public.partition_table2_1_prt_girls",
					"public.partition_table2_1_prt_other",
				}
			} else {
				// For GPDB 7+, each leaf partition of the included partition_table1 will have their own
				// separate DDL along with an ATTACH PARTITION. Since only partition_table2_1_prt_other
				// is included, only it and its root partition will be a part of the list.
				expectedTableNames = []string{
					"public.partition_table1",
					"public.partition_table1_1_prt_boys",
					"public.partition_table1_1_prt_girls",
					"public.partition_table1_1_prt_other",
					"public.partition_table2",
					"public.partition_table2_1_prt_other",
				}
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
		It("returns included parent tables but omits un-included external partitions if the filter includes a parent partition table and no-inherits is set", func() {
			_ = backupCmdFlags.Set(options.NO_INHERITS, "true")
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.partition_table1")
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.partition_table2_1_prt_other")
			_ = backupCmdFlags.Set(options.LEAF_PARTITION_DATA, "true")

			createPartitionTableWithExternalPartition("public.partition_table1", "boys")
			defer dropTableWithExternalPartition("public.partition_table1")
			createPartitionTableWithExternalPartition("public.partition_table2", "girls")
			defer dropTableWithExternalPartition("public.partition_table2")
			createPartitionTableWithExternalPartition("public.partition_table3", "girls")
			defer dropTableWithExternalPartition("public.partition_table3")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			var expectedTableNames []string
			expectedTableNames = []string{
				"public.partition_table1",
				"public.partition_table2_1_prt_other",
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
		It("returns parent and child tables if the filter includes a non-partition table with multiple inheritance relationships", func() {
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.base")
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.parent_one(one int);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.parent_two(two int);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.base() INHERITS (public.parent_one, public.parent_two);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.child_one() INHERITS (public.base);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.child_two() INHERITS (public.base);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.unrelated(three int);`)
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.parent_one CASCADE")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.parent_two CASCADE")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.unrelated")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			expectedTableNames := []string{
				"public.base",
				"public.child_one",
				"public.child_two",
				"public.parent_one",
				"public.parent_two",
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
		It("returns parent tables but not child tables if the filter includes a non-partition table with multiple inheritance relationships and no-inherits is set", func() {
			_ = backupCmdFlags.Set(options.NO_INHERITS, "true")
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.base")
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.parent_one(one int);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.parent_two(two int);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.base() INHERITS (public.parent_one, public.parent_two);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.child_one() INHERITS (public.base);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.child_two() INHERITS (public.base);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.unrelated(three int);`)
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.parent_one CASCADE")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.parent_two CASCADE")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.unrelated")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			expectedTableNames := []string{
				"public.base",
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
		It("returns parents of child tables if the children of an included table inherit from other tables", func() {
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.base")
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.base(one int);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.other(two int);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.child() INHERITS (public.base);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.grandchild() INHERITS (public.child, public.other);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.unrelated(three int);`)
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.base CASCADE")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.other CASCADE")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.unrelated")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			expectedTableNames := []string{
				"public.base",
				"public.child",
				"public.grandchild",
				"public.other",
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
		It("returns only the included table, without any children or other parents of children, if no-inherits is passed, ", func() {
			_ = backupCmdFlags.Set(options.NO_INHERITS, "true")
			_ = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.base")
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.base(one int);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.other(two int);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.child() INHERITS (public.base);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.grandchild() INHERITS (public.child, public.other);`)
			testhelper.AssertQueryRuns(connectionPool, `CREATE TABLE public.unrelated(three int);`)
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.base CASCADE")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.other CASCADE")
			defer testhelper.AssertQueryRuns(connectionPool, "DROP TABLE public.unrelated")

			subject, err := options.NewOptions(backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))
			backup.ValidateAndProcessFilterLists(subject)

			includeOids := backup.GetOidsFromRelationList(backup.IncludedRelationFqns)
			err = backup.ExpandIncludesForPartitions(connectionPool, subject, includeOids, backupCmdFlags)
			Expect(err).To(Not(HaveOccurred()))

			expectedTableNames := []string{
				"public.base",
			}

			tables := subject.GetIncludedTables()
			sort.Strings(tables)
			Expect(tables).To(Equal(expectedTableNames))
		})
	})
})
