package restore_test

import (
	"strings"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/apache/cloudberry-backup/history"
	"github.com/apache/cloudberry-backup/options"
	"github.com/apache/cloudberry-backup/restore"
	"github.com/apache/cloudberry-backup/testutils"
	"github.com/apache/cloudberry-backup/toc"
	"github.com/apache/cloudberry-backup/utils"
	"github.com/apache/cloudberry-go-libs/testhelper"
	"github.com/spf13/cobra"

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

var _ = Describe("restore/validate tests", func() {
	var filterList []string
	var tocfile *toc.TOC
	var backupfile *utils.FileWithByteCount
	AfterEach(func() {
		filterList = []string{}
	})
	Describe("ValidateSchemasInBackupSet", func() {
		sequence := toc.StatementWithType{ObjectType: toc.OBJ_SEQUENCE, Statement: "CREATE SEQUENCE schema.somesequence"}
		sequenceLen := uint64(len(sequence.Statement))
		table1 := toc.StatementWithType{ObjectType: toc.OBJ_TABLE, Statement: "CREATE TABLE schema1.table1"}
		table1Len := uint64(len(table1.Statement))
		table2 := toc.StatementWithType{ObjectType: toc.OBJ_TABLE, Statement: "CREATE TABLE schema2.table2"}
		table2Len := uint64(len(table2.Statement))
		BeforeEach(func() {
			tocfile, backupfile = testutils.InitializeTestTOC(buffer, "predata")
			backupfile.ByteCount = table1Len
			tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "table1", ObjectType: toc.OBJ_TABLE}, 0, backupfile.ByteCount, []uint32{0, 0})
			tocfile.AddCoordinatorDataEntry("schema1", "table1", 1, "(i)", 0, "", "", false)
			backupfile.ByteCount += table2Len
			tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema2", Name: "table2", ObjectType: toc.OBJ_TABLE}, table1Len, backupfile.ByteCount, []uint32{0, 0})
			tocfile.AddCoordinatorDataEntry("schema2", "table2", 2, "(j)", 0, "", "", false)
			backupfile.ByteCount += sequenceLen
			tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema", Name: "somesequence", ObjectType: toc.OBJ_SEQUENCE}, table1Len+table2Len, backupfile.ByteCount, []uint32{0, 0})
			restore.SetTOC(tocfile)
		})
		It("passes when schema exists in normal backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema1"}
			restore.ValidateIncludeSchemasInBackupSet(filterList)
		})
		It("panics when schema does not exist in normal backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema3"}
			defer testhelper.ShouldPanicWithMessage("Could not find the following schema(s) in the backup set: schema3")
			restore.ValidateIncludeSchemasInBackupSet(filterList)
		})
		It("passes when schema exists in data-only backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{DataOnly: true})
			filterList = []string{"schema1"}
			restore.ValidateIncludeSchemasInBackupSet(filterList)
		})
		It("panics when schema does not exist in data-only backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{DataOnly: true})
			filterList = []string{"schema3"}
			defer testhelper.ShouldPanicWithMessage("Could not find the following schema(s) in the backup set: schema3")
			restore.ValidateIncludeSchemasInBackupSet(filterList)
		})
		It("generates warning when exclude-schema does not exist in backup and noFatal is true", func() {
			_, _, logfile = testhelper.SetupTestLogger()
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema3"}
			restore.ValidateExcludeSchemasInBackupSet(filterList)
			testhelper.ExpectRegexp(logfile, "[WARNING]:-Could not find the following excluded schema(s) in the backup set: schema3")
		})
	})
	Describe("GenerateRestoreRelationList", func() {
		var opts *options.Options
		BeforeEach(func() {
			tocfile, _ = testutils.InitializeTestTOC(buffer, "metadata")
			tocfile.AddCoordinatorDataEntry("s1", "table1", 1, "(j)", 0, "", "", false)
			tocfile.AddCoordinatorDataEntry("s1", "table2", 2, "(j)", 0, "", "", false)
			tocfile.AddCoordinatorDataEntry("s2", "table1", 3, "(j)", 0, "", "", false)
			tocfile.AddCoordinatorDataEntry("s2", "table2", 4, "(j)", 0, "", "", false)
			restore.SetTOC(tocfile)

			opts = &options.Options{}
		})
		It("returns all tables if no filtering is used", func() {
			opts.IncludedRelations = []string{"s1.table1", "s1.table2", "s2.table1", "s2.table2"}

			resultRelations := restore.GenerateRestoreRelationList(*opts)

			expectedRelations := []string{"s1.table1", "s1.table2", "s2.table1", "s2.table2"}
			Expect(resultRelations).To(ConsistOf(expectedRelations))
		})
		It("filters on include relations", func() {
			opts.IncludedRelations = []string{"s1.table1", "s1.table2"}

			resultRelations := restore.GenerateRestoreRelationList(*opts)

			expectedRelations := []string{"s1.table1", "s1.table2"}
			Expect(resultRelations).To(ConsistOf(expectedRelations))
		})
		It("filters on exclude relations", func() {
			opts.ExcludedRelations = []string{"s1.table2", "s2.table1"}

			resultRelations := restore.GenerateRestoreRelationList(*opts)

			expectedRelations := []string{"s1.table1", "s2.table2"}
			Expect(resultRelations).To(ConsistOf(expectedRelations))
		})
		It("filters on include schema", func() {
			opts.IncludedSchemas = []string{"s1"}

			resultRelations := restore.GenerateRestoreRelationList(*opts)

			expectedRelations := []string{"s1.table1", "s1.table2"}
			Expect(resultRelations).To(ConsistOf(expectedRelations))
		})
		It("filters on exclude schema", func() {
			opts.ExcludedSchemas = []string{"s2"}

			resultRelations := restore.GenerateRestoreRelationList(*opts)

			expectedRelations := []string{"s1.table1", "s1.table2"}
			Expect(resultRelations).To(ConsistOf(expectedRelations))
		})
		It("filters on include schema with exclude relation", func() {
			opts.IncludedSchemas = []string{"s1"}
			opts.ExcludedRelations = []string{"s1.table1"}

			resultRelations := restore.GenerateRestoreRelationList(*opts)

			expectedRelations := []string{"s1.table2"}
			Expect(resultRelations).To(ConsistOf(expectedRelations))
		})
	})
	Describe("ValidateRelationsInRestoreDatabase", func() {
		BeforeEach(func() {
			restore.SetBackupConfig(&history.BackupConfig{DataOnly: false})
			_ = cmdFlags.Set(options.DATA_ONLY, "false")
		})
		Context("data-only restore", func() {
			BeforeEach(func() {
				_ = cmdFlags.Set(options.DATA_ONLY, "true")
			})
			It("panics if all tables missing from database", func() {
				noTableRows := sqlmock.NewRows([]string{"string"})
				mock.ExpectQuery("SELECT (.*)").WillReturnRows(noTableRows)
				filterList = []string{"public.table2"}
				defer testhelper.ShouldPanicWithMessage("Relation public.table2 must exist for data-only restore")
				restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList)
			})
			It("panics if some tables missing from database", func() {
				singleTableRow := sqlmock.NewRows([]string{"string"}).
					AddRow("public.table1")
				mock.ExpectQuery("SELECT (.*)").WillReturnRows(singleTableRow)
				filterList = []string{"public.table1", "public.table2"}
				defer testhelper.ShouldPanicWithMessage("Relation public.table2 must exist for data-only restore")
				restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList)
			})
			It("passes if all tables are present in database", func() {
				twoTableRows := sqlmock.NewRows([]string{"string"}).
					AddRow("public.table1").AddRow("public.table2")
				mock.ExpectQuery("SELECT (.*)").WillReturnRows(twoTableRows)
				filterList = []string{"public.table1", "public.table2"}
				restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList)
			})
		})
		Context("restore includes metadata", func() {
			It("passes if table is not present in database", func() {
				noTableRows := sqlmock.NewRows([]string{"string"})
				mock.ExpectQuery("SELECT (.*)").WillReturnRows(noTableRows)
				filterList = []string{"public.table2"}
				restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList)
			})
			It("panics if single table is present in database", func() {
				singleTableRow := sqlmock.NewRows([]string{"string"}).
					AddRow("public.table1")
				mock.ExpectQuery("SELECT (.*)").WillReturnRows(singleTableRow)
				filterList = []string{"public.table1", "public.table2"}
				defer testhelper.ShouldPanicWithMessage("Relation public.table1 already exists")
				restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList)
			})
			It("panics if multiple tables are present in database", func() {
				twoTableRows := sqlmock.NewRows([]string{"string"}).
					AddRow("public.table1").AddRow("public.view1")
				mock.ExpectQuery("SELECT (.*)").WillReturnRows(twoTableRows)
				filterList = []string{"public.table1", "public.view1"}
				defer testhelper.ShouldPanicWithMessage("Relation public.table1 already exists")
				restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList)
			})
		})
	})
	Describe("ValidateRelationsInBackupSet", func() {
		var tocfile *toc.TOC
		var backupfile *utils.FileWithByteCount
		BeforeEach(func() {
			tocfile, backupfile = testutils.InitializeTestTOC(buffer, "predata")
			tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "table1", ObjectType: toc.OBJ_TABLE}, 0, backupfile.ByteCount, []uint32{0, 0})
			tocfile.AddCoordinatorDataEntry("schema1", "table1", 1, "(i)", 0, "", "", false)

			tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema2", Name: "table2", ObjectType: toc.OBJ_TABLE}, 0, backupfile.ByteCount, []uint32{0, 0})
			tocfile.AddCoordinatorDataEntry("schema2", "table2", 2, "(j)", 0, "", "", false)

			tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "somesequence", ObjectType: toc.OBJ_SEQUENCE}, 0, backupfile.ByteCount, []uint32{0, 0})
			tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "someview", ObjectType: toc.OBJ_VIEW}, 0, backupfile.ByteCount, []uint32{0, 0})
			tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "somefunction", ObjectType: toc.OBJ_FUNCTION}, 0, backupfile.ByteCount, []uint32{0, 0})

			restore.SetTOC(tocfile)
		})
		It("passes when table exists in normal backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema1.table1"}
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
		It("panics when table does not exist in normal backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema1.table3"}
			defer testhelper.ShouldPanicWithMessage("Could not find the following relation(s) in the backup set: schema1.table3")
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
		It("passes when sequence exists in normal backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema1.somesequence"}
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
		It("generates a warning if the exclude-schema is not in the backup set and noFatal is true", func() {
			_, _, logfile = testhelper.SetupTestLogger()
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema1.table3"}
			restore.ValidateExcludeRelationsInBackupSet(filterList)
			testhelper.ExpectRegexp(logfile, "[WARNING]:-Could not find the following excluded relation(s) in the backup set: schema1.table3")
		})
		It("passes when view exists in normal backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema1.someview"}
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
		It("passes when table exists in data-only backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{DataOnly: true})
			filterList = []string{"schema1.table1"}
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
		It("panics when relation does not exist in backup but function with same name does", func() {
			restore.SetBackupConfig(&history.BackupConfig{})
			filterList = []string{"schema1.somefunction"}
			defer testhelper.ShouldPanicWithMessage("Could not find the following relation(s) in the backup set: schema1.somefunction")
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
		It("table does not exist in data-only backup", func() {
			restore.SetBackupConfig(&history.BackupConfig{DataOnly: true})
			filterList = []string{"schema1.table3"}
			defer testhelper.ShouldPanicWithMessage("Could not find the following relation(s) in the backup set: schema1.table3")
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
		It("passes when table exists in most recent restore plan entry", func() {
			restore.SetBackupConfig(&history.BackupConfig{RestorePlan: []history.RestorePlanEntry{{TableFQNs: []string{"schema1.table1_part_1"}}}})
			filterList = []string{"schema1.table1_part_1"}
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
		It("passes when table exists in previous restore plan entry", func() {
			restore.SetBackupConfig(&history.BackupConfig{RestorePlan: []history.RestorePlanEntry{{TableFQNs: []string{"schema1.random_table"}}, {TableFQNs: []string{"schema1.table1_part_1"}}}})
			filterList = []string{"schema1.table1_part_1"}
			restore.ValidateIncludeRelationsInBackupSet(filterList)
		})
	})
	Describe("ValidateDatabaseExistence", func() {
		It("panics if createdb passed when db exists", func() {
			dbExists := sqlmock.NewRows([]string{"string"}).
				AddRow("true")
			mock.ExpectQuery("SELECT (.*)").WillReturnRows(dbExists)
			defer testhelper.ShouldPanicWithMessage(`Database "testdb" already exists.`)
			restore.ValidateDatabaseExistence("testdb", true, false)
		})
		It("passes if db exists and --create-db not passed", func() {
			dbExists := sqlmock.NewRows([]string{"string"}).
				AddRow("true")
			mock.ExpectQuery("SELECT (.*)").WillReturnRows(dbExists)
			restore.ValidateDatabaseExistence("testdb", false, false)
		})
		It("panics and tells user to manually create db when db does not exist and filtered", func() {
			dbExists := sqlmock.NewRows([]string{"string"}).
				AddRow("false")
			mock.ExpectQuery("SELECT (.*)").WillReturnRows(dbExists)
			defer testhelper.ShouldPanicWithMessage(`Database "testdb" must be created manually`)
			restore.ValidateDatabaseExistence("testdb", true, true)
		})
		It("panics and tells user to pass --create-db when db does not exist, not filtered, and no --create-db", func() {
			dbExists := sqlmock.NewRows([]string{"string"}).
				AddRow("false")
			mock.ExpectQuery("SELECT (.*)").WillReturnRows(dbExists)
			defer testhelper.ShouldPanicWithMessage(`Database "testdb" does not exist. Use the --create-db flag`)
			restore.ValidateDatabaseExistence("testdb", false, false)
		})
	})
	Describe("Validate various flag combinations that are required or exclusive", func() {
		DescribeTable("Validate various flag combinations that are required or exclusive",
			func(argString string, valid bool) {
				testCmd := &cobra.Command{
					Use:  "flag validation",
					Args: cobra.NoArgs,
					Run: func(cmd *cobra.Command, args []string) {
						restore.ValidateFlagCombinations(cmd.Flags())
					}}
				testCmd.SetArgs(strings.Split(argString, " "))
				restore.SetCmdFlags(testCmd.Flags())

				if !valid {
					defer testhelper.ShouldPanicWithMessage("CRITICAL")
				}

				err := testCmd.Execute()
				if err != nil && valid {
					Fail("Valid flag combination failed validation check")
				}
			},
			Entry("--backup-dir combo", "--backup-dir /tmp --plugin-config /tmp/config", false),

			/*
			 * Below are all the different filter combinations
			 */
			// --exclude-schema combinations with other filters
			Entry("--exclude-schema combos", "--timestamp=0 --exclude-schema schema1 --include-table schema.table2", false),
			Entry("--exclude-schema combos", "--timestamp=0 --exclude-schema schema1 --include-table-file /tmp/file2", false),
			Entry("--exclude-schema combos", "--timestamp=0 --exclude-schema schema1 --include-schema schema2", false),
			Entry("--exclude-schema combos", "--timestamp=0 --exclude-schema schema1 --include-schema-file /tmp/file2", true), // TODO: Verify this.
			Entry("--exclude-schema combos", "--timestamp=0 --exclude-schema schema1 --exclude-table schema.table2", false),
			Entry("--exclude-schema combos", "--timestamp=0 --exclude-schema schema1 --exclude-table-file /tmp/file2", false),
			Entry("--exclude-schema combos", "--timestamp=0 --exclude-schema schema1 --exclude-schema schema2", true),
			Entry("--exclude-schema combos", "--timestamp=0 --exclude-schema schema1 --exclude-schema-file /tmp/file2", true), // TODO: Verify this.

			// --exclude-schema-file combinations with other filters
			Entry("--exclude-schema-file combos", "--timestamp=0 --exclude-schema-file /tmp/file --include-table schema.table2", true),    // TODO: Verify this.
			Entry("--exclude-schema-file combos", "--timestamp=0 --exclude-schema-file /tmp/file --include-table-file /tmp/file2", true),  // TODO: Verify this.
			Entry("--exclude-schema-file combos", "--timestamp=0 --exclude-schema-file /tmp/file --include-schema schema2", true),         // TODO: Verify this.
			Entry("--exclude-schema-file combos", "--timestamp=0 --exclude-schema-file /tmp/file --include-schema-file /tmp/file2", true), // TODO: Verify this.
			Entry("--exclude-schema-file combos", "--timestamp=0 --exclude-schema-file /tmp/file --exclude-table schema.table2", true),    // TODO: Verify this.
			Entry("--exclude-schema-file combos", "--timestamp=0 --exclude-schema-file /tmp/file --exclude-table-file /tmp/file2", true),  // TODO: Verify this.

			// --exclude-table combinations with other filters
			Entry("--exclude-table combos", "--timestamp=0 --exclude-table schema.table --include-table schema.table2", false),
			Entry("--exclude-table combos", "--timestamp=0 --exclude-table schema.table --include-table-file /tmp/file2", false),
			Entry("--exclude-table combos", "--timestamp=0 --exclude-table schema.table --include-schema schema2", true),
			Entry("--exclude-table combos", "--timestamp=0 --exclude-table schema.table --include-schema-file /tmp/file2", true),
			Entry("--exclude-table combos", "--timestamp=0 --exclude-table schema.table --exclude-table schema.table2", true),
			Entry("--exclude-table combos", "--timestamp=0 --exclude-table schema.table --exclude-table-file /tmp/file2", false),

			// --exclude-table-file combinations with other filters
			Entry("--exclude-table-file combos", "--timestamp=0 --exclude-table-file /tmp/file --include-table schema.table2", false),
			Entry("--exclude-table-file combos", "--timestamp=0 --exclude-table-file /tmp/file --include-table-file /tmp/file2", false),
			Entry("--exclude-table-file combos", "--timestamp=0 --exclude-table-file /tmp/file --include-schema schema2", true),
			Entry("--exclude-table-file combos", "--timestamp=0 --exclude-table-file /tmp/file --include-schema-file /tmp/file2", true),

			// --include-schema combinations with other filters
			Entry("--include-schema combos", "--timestamp=0 --include-schema schema1 --include-table schema.table2", false),
			Entry("--include-schema combos", "--timestamp=0 --include-schema schema1 --include-table-file /tmp/file2", false),
			Entry("--include-schema combos", "--timestamp=0 --include-schema schema1 --include-schema schema2", true),
			Entry("--include-schema combos", "--timestamp=0 --include-schema schema1 --include-schema-file /tmp/file2", true), // TODO: Verify this.

			// --include-schema-file combinations with other filters
			Entry("--include-schema-file combos", "--timestamp=0 --include-schema-file /tmp/file --include-table schema.table2", true),   // TODO: Verify this.
			Entry("--include-schema-file combos", "--timestamp=0 --include-schema-file /tmp/file --include-table-file /tmp/file2", true), // TODO: Verify this.

			// --include-table combinations with other filters
			Entry("--include-table combos", "--timestamp=0 --include-table schema.table --include-table schema.table2", true),
			Entry("--include-table combos", "--timestamp=0 --include-table schema.table --include-table-file /tmp/file2", false),

			/*
			 * Below are various different incremental combinations
			 */
			Entry("incremental combos", "--timestamp=0 --incremental", false),
			Entry("incremental combos", "--timestamp=0 --incremental --data-only", true),

			/*
			 * Below are various different truncate combinations
			 */
			Entry("truncate combos", "--timestamp=0 --truncate-table", false),
			Entry("truncate combos", "--timestamp=0 --truncate-table --include-table schema.table2", true),
			Entry("truncate combos", "--timestamp=0 --truncate-table --include-table-file /tmp/file2", true),
			Entry("truncate combos", "--timestamp=0 --truncate-table --include-table schema.table2 --redirect-db foodb", true),
			Entry("truncate combos", "--timestamp=0 --truncate-table --include-table schema.table2 --redirect-schema schema2", false),

			/*
			 * Below are various different redirect-schema combinations
			 */
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1", false),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --include-table schema.table2", true),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --include-table-file /tmp/file2", true),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --include-schema schema2", true),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --include-schema-file /tmp/file2", true),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --exclude-table schema.table2", false),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --exclude-table-file /tmp/file2", false),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --exclude-schema schema2", false),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --exclude-schema-file /tmp/file2", false),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --include-table schema.table2 --metadata-only", true),
			Entry("--redirect-schema combos", "--timestamp=0 --redirect-schema schema1 --include-table schema.table2 --data-only", true),
		)
	})
	Describe("ValidateBackupFlagCombinations", func() {
		It("restore with copy-queue-size should fatal if backup was not taken with single-data-file", func() {
			restore.SetBackupConfig(&history.BackupConfig{SingleDataFile: false})
			testCmd := &cobra.Command{
				Use:  "flag validation",
				Args: cobra.NoArgs,
				Run: func(cmd *cobra.Command, args []string) {
					restore.ValidateBackupFlagCombinations()
				}}
			testCmd.SetArgs([]string{"--copy-queue-size", "4"})
			restore.SetCmdFlags(testCmd.Flags())

			defer testhelper.ShouldPanicWithMessage("CRITICAL")
			err := testCmd.Execute()
			if err == nil {
				Fail("invalid flag combination passed validation check")
			}
		})
	})
})
