package backup_test

import (
	"fmt"

	"github.com/apache/cloudberry-backup/backup"
	"github.com/apache/cloudberry-backup/testutils"
	"github.com/apache/cloudberry-backup/toc"
	"github.com/lib/pq"

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

var _ = Describe("backup/statistics tests", func() {
	getStatInsertReplace := func(smallint int, oid int) (string, string, string, string, string) {
		insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5 := "", "", "", "", ""
		if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
			insertReplace1 = `
	false::boolean,`
			insertReplace2 = fmt.Sprintf(`
	%d::smallint,`, smallint)
			insertReplace3 = fmt.Sprintf(`
	%d::oid,`, oid)
			insertReplace4 = `
	NULL::real[],`
			insertReplace5 = `
	NULL,`
		}

		// GPDB 7+ has collations
		if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
			insertReplace3 = insertReplace3 + `
	0::oid,
	0::oid,
	0::oid,
	0::oid,
	0::oid,`
		}

		return insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5
	}

	Describe("PrintStatisticsStatementsForTable", func() {
		It("prints tuple stats and attr stats for all tables", func() {
			tocfile, backupfile = testutils.InitializeTestTOC(buffer, "statistics")

			testTable1 := backup.Table{Relation: backup.Relation{Oid: 123, Schema: "testschema", Name: "testtable1"}}
			tupleStat1 := backup.TupleStatistic{Schema: "testschema", Table: "testtable1"}
			attStat1 := []backup.AttributeStatistic{}

			testTable2 := backup.Table{Relation: backup.Relation{Oid: 456, Schema: "testschema", Name: "testtable2"}}
			tupleStat2 := backup.TupleStatistic{Schema: "testschema", Table: "testtable2"}
			attStat2 := []backup.AttributeStatistic{
				{Schema: "testschema", Table: "testtable2", AttName: "testattWithArray", Type: "_array"},
				{Schema: "testschema", Table: "testtable2", AttName: "testatt", Type: "_array", Relid: 2, AttNumber: 3, NullFraction: .4,
					Width: 10, Distinct: .5, Kind1: 20, Operator1: 10, Numbers1: []string{"1", "2", "3"}, Values1: []string{"4", "5", "6"}},
			}

			tables := []backup.Table{testTable1, testTable2}
			tupleStats := map[uint32]backup.TupleStatistic{
				123: tupleStat1,
				456: tupleStat2,
			}
			attStats := map[uint32][]backup.AttributeStatistic{
				123: attStat1,
				456: attStat2,
			}

			backup.PrintStatisticsStatements(backupfile, tocfile, tables, attStats, tupleStats)
			testutils.ExpectEntry(tocfile.StatisticsEntries, 0, "testschema", "", "testtable1", toc.OBJ_STATISTICS)
			testutils.ExpectEntry(tocfile.StatisticsEntries, 1, "testschema", "", "testtable2", toc.OBJ_STATISTICS)
			testutils.ExpectEntry(tocfile.StatisticsEntries, 2, "testschema", "", "testtable2", toc.OBJ_STATISTICS)
			testutils.ExpectEntry(tocfile.StatisticsEntries, 3, "testschema", "", "testtable2", toc.OBJ_STATISTICS)
			testutils.ExpectEntry(tocfile.StatisticsEntries, 4, "testschema", "", "testtable2", toc.OBJ_STATISTICS)
			testutils.ExpectEntry(tocfile.StatisticsEntries, 5, "testschema", "", "testtable2", toc.OBJ_STATISTICS)

			insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5 := getStatInsertReplace(0, 0)

			expected := []string{
				`UPDATE pg_class
SET
	relpages = 0::int,
	reltuples = 0.000000::real
WHERE oid = 'testschema.testtable1'::regclass::oid;`,

				`UPDATE pg_class
SET
	relpages = 0::int,
	reltuples = 0.000000::real
WHERE oid = 'testschema.testtable2'::regclass::oid;`,

				`DELETE FROM pg_statistic WHERE starelid = 'testschema.testtable2'::regclass::oid AND staattnum = 0;`,

				fmt.Sprintf(`INSERT INTO pg_statistic VALUES (
	'testschema.testtable2'::regclass::oid,
	0::smallint,%[1]s
	0.000000::real,
	0::integer,
	0.000000::real,
	0::smallint,
	0::smallint,
	0::smallint,
	0::smallint,%[2]s
	0::oid,
	0::oid,
	0::oid,
	0::oid,%[3]s
	NULL::real[],
	NULL::real[],
	NULL::real[],
	NULL::real[],%[4]s
	NULL,
	NULL,
	NULL,%[5]s
	NULL);`, insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5),

				`DELETE FROM pg_statistic WHERE starelid = 'testschema.testtable2'::regclass::oid AND staattnum = 3;`,

				fmt.Sprintf(`INSERT INTO pg_statistic VALUES (
	'testschema.testtable2'::regclass::oid,
	3::smallint,%[1]s
	0.400000::real,
	10::integer,
	0.500000::real,
	0::smallint,
	0::smallint,
	0::smallint,
	0::smallint,%[2]s
	0::oid,
	0::oid,
	0::oid,
	0::oid,%[3]s
	NULL::real[],
	NULL::real[],
	NULL::real[],
	NULL::real[],%[4]s
	NULL,
	NULL,
	NULL,%[5]s
	NULL);`, insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5),
			}
			testutils.AssertBufferContents(tocfile.StatisticsEntries, buffer, expected...)
		})
	})
	Describe("GenerateTupleStatisticsQuery", func() {
		It("generates tuple statistics query with double quotes and a single quote in the table name and schema name", func() {
			tableTestTable := backup.Table{Relation: backup.Relation{Schema: `"""test'schema"""`, Name: `"""test'table"""`}}
			tupleStats := backup.TupleStatistic{Schema: `"""test'schema"""`, Table: `"""test'table"""`}
			tupleQuery := backup.GenerateTupleStatisticsQuery(tableTestTable, tupleStats)
			Expect(tupleQuery).To(Equal(`UPDATE pg_class
SET
	relpages = 0::int,
	reltuples = 0.000000::real
WHERE oid = '"""test''schema"""."""test''table"""'::regclass::oid;`))
		})

	})
	Describe("GenerateAttributeStatisticsQueries", func() {
		tableTestTable := backup.Table{Relation: backup.Relation{Schema: "testschema", Name: `"test'table"`}}

		It("generates attribute statistics query for array type", func() {
			attStats := backup.AttributeStatistic{Schema: "testschema", Table: "testtable", AttName: "testatt", Type: "_array", Relid: 2,
				AttNumber: 3, NullFraction: .4, Width: 10, Distinct: .5, Kind1: 20, Operator1: 10,
				Numbers1: pq.StringArray([]string{"1", "2", "3"}), Values1: pq.StringArray([]string{"4", "5", "6"})}
			if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
				attStats.Kind5 = 10
				attStats.Operator5 = 12
			}

			attStatsQueries := backup.GenerateAttributeStatisticsQueries(tableTestTable, attStats)
			Expect(attStatsQueries[0]).To(Equal(fmt.Sprintf(`DELETE FROM pg_statistic WHERE starelid = 'testschema."test''table"'::regclass::oid AND staattnum = 3;`)))

			insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5 := getStatInsertReplace(0, 0)
			Expect(attStatsQueries[1]).To(Equal(fmt.Sprintf(`INSERT INTO pg_statistic VALUES (
	'testschema."test''table"'::regclass::oid,
	3::smallint,%s
	0.400000::real,
	10::integer,
	0.500000::real,
	0::smallint,
	0::smallint,
	0::smallint,
	0::smallint,%s
	0::oid,
	0::oid,
	0::oid,
	0::oid,%s
	NULL::real[],
	NULL::real[],
	NULL::real[],
	NULL::real[],%s
	NULL,
	NULL,
	NULL,%s
	NULL);`, insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5)))
		})
		It("generates attribute statistics query for non-array type", func() {
			attStats := backup.AttributeStatistic{Schema: "testschema", Table: "testtable", AttName: "testatt", Type: "testtype", Relid: 2,
				AttNumber: 3, NullFraction: .4, Width: 10, Distinct: .5, Kind1: 20, Operator1: 10,
				Numbers1: pq.StringArray([]string{"1", "2", "3"}), Values1: pq.StringArray([]string{"4", "5", "6"})}
			if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
				attStats.Kind5 = 10
				attStats.Operator5 = 12
			}

			attStatsQueries := backup.GenerateAttributeStatisticsQueries(tableTestTable, attStats)

			Expect(attStatsQueries[0]).To(Equal(fmt.Sprintf(`DELETE FROM pg_statistic WHERE starelid = 'testschema."test''table"'::regclass::oid AND staattnum = 3;`)))

			insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5 := getStatInsertReplace(10, 12)
			Expect(attStatsQueries[1]).To(Equal(fmt.Sprintf(`INSERT INTO pg_statistic VALUES (
	'testschema."test''table"'::regclass::oid,
	3::smallint,%s
	0.400000::real,
	10::integer,
	0.500000::real,
	20::smallint,
	0::smallint,
	0::smallint,
	0::smallint,%s
	10::oid,
	0::oid,
	0::oid,
	0::oid,%s
	'{"1","2","3"}'::real[],
	NULL::real[],
	NULL::real[],
	NULL::real[],%s
	array_in('{"4","5","6"}', 'testtype'::regtype::oid, -1),
	NULL,
	NULL,%s
	NULL);`, insertReplace1, insertReplace2, insertReplace3, insertReplace4, insertReplace5)))
		})
	})
	Describe("AnyValues", func() {
		It("returns properly casted string when length of anyvalues is greater than 0", func() {
			castedString := backup.AnyValues([]string{"1", "2"}, "int", 1)
			Expect(castedString).To(Equal(`array_in('{"1","2"}', 'int'::regtype::oid, -1)`))
		})
		It("returns int8 casted string when kind is STATISTIC_KIND_NDV_BY_SEGMENTS", func() {
			castedString := backup.AnyValues([]string{"2"}, "bool", backup.STATISTIC_KIND_NDV_BY_SEGMENTS)
			Expect(castedString).To(Equal(`array_in('{"2"}', 'int8'::regtype::oid, -1)`))
		})
		It("returns NULL if anyvalues is of length 0", func() {
			castedString := backup.AnyValues([]string{}, "int", 1)
			Expect(castedString).To(Equal(`NULL`))
		})
	})
	Describe("SliceToPostgresArray", func() {
		It("returns properly quoted string representing a Postgres array", func() {
			arrayString := backup.SliceToPostgresArray([]string{"ab'c", "ab\\c", "ab\"c", "ef\\'\"g"})
			Expect(arrayString).To(Equal("'{\"ab''c\",\"ab\\\\c\",\"ab\\\"c\",\"ef\\\\''\\\"g\"}'"))
		})
	})
})
