package sqlite

import (
	"database/sql"
	"testing"

	"gorm.io/gorm/migrator"
	"gorm.io/gorm/utils/tests"
)

func TestParseDDL(t *testing.T) {
	params := []struct {
		name    string
		sql     []string
		nFields int
		columns []migrator.ColumnType
	}{
		{"with_fk", []string{
			"CREATE TABLE `notes` (" +
				"`id` integer NOT NULL,`text` varchar(500) DEFAULT \"hello\",`age` integer DEFAULT 18,`user_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))",
			"CREATE UNIQUE INDEX `idx_profiles_refer` ON `profiles`(`text`)",
		}, 6, []migrator.ColumnType{
			{NameValue: sql.NullString{String: "id", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, PrimaryKeyValue: sql.NullBool{Bool: true, Valid: true}, NullableValue: sql.NullBool{Valid: true}, UniqueValue: sql.NullBool{Valid: true}, DefaultValueValue: sql.NullString{Valid: false}},
			{NameValue: sql.NullString{String: "text", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 500, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(500)", Valid: true}, DefaultValueValue: sql.NullString{String: "hello", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Bool: false, Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
			{NameValue: sql.NullString{String: "age", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, DefaultValueValue: sql.NullString{String: "18", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
			{NameValue: sql.NullString{String: "user_id", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
		},
		},
		{"with_check", []string{"CREATE TABLE Persons (ID int NOT NULL,LastName varchar(255) NOT NULL,FirstName varchar(255),Age int,CHECK (Age>=18),CHECK (FirstName<>'John'))"}, 6, []migrator.ColumnType{
			{NameValue: sql.NullString{String: "ID", Valid: true}, DataTypeValue: sql.NullString{String: "int", Valid: true}, ColumnTypeValue: sql.NullString{String: "int", Valid: true}, NullableValue: sql.NullBool{Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
			{NameValue: sql.NullString{String: "LastName", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 255, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(255)", Valid: true}, NullableValue: sql.NullBool{Bool: false, Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
			{NameValue: sql.NullString{String: "FirstName", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 255, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(255)", Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
			{NameValue: sql.NullString{String: "Age", Valid: true}, DataTypeValue: sql.NullString{String: "int", Valid: true}, ColumnTypeValue: sql.NullString{String: "int", Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
		}},
		{"lowercase", []string{"create table test (ID int NOT NULL)"}, 1, []migrator.ColumnType{
			{NameValue: sql.NullString{String: "ID", Valid: true}, DataTypeValue: sql.NullString{String: "int", Valid: true}, ColumnTypeValue: sql.NullString{String: "int", Valid: true}, NullableValue: sql.NullBool{Bool: false, Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
		},
		},
		{"no brackets", []string{"create table test"}, 0, nil},
		{"with_special_characters", []string{
			"CREATE TABLE `test` (`text` varchar(10) DEFAULT \"测试, \")",
		}, 1, []migrator.ColumnType{
			{NameValue: sql.NullString{String: "text", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 10, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(10)", Valid: true}, DefaultValueValue: sql.NullString{String: "测试, ", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
		},
		},
		{
			"table_name_with_dash",
			[]string{
				"CREATE TABLE `test-a` (`id` int NOT NULL)",
				"CREATE UNIQUE INDEX `idx_test-a_id` ON `test-a`(`id`)",
			},
			1,
			[]migrator.ColumnType{
				{
					NameValue:         sql.NullString{String: "id", Valid: true},
					DataTypeValue:     sql.NullString{String: "int", Valid: true},
					ColumnTypeValue:   sql.NullString{String: "int", Valid: true},
					NullableValue:     sql.NullBool{Bool: false, Valid: true},
					DefaultValueValue: sql.NullString{Valid: false},
					UniqueValue:       sql.NullBool{Bool: false, Valid: true},
					PrimaryKeyValue:   sql.NullBool{Valid: true},
				},
			},
		}, {
			"unique index",
			[]string{
				"CREATE TABLE `test-b` (`field` integer NOT NULL)",
				"CREATE UNIQUE INDEX `idx_uq` ON `test-b`(`field`) WHERE field = 0",
			},
			1,
			[]migrator.ColumnType{{
				NameValue:       sql.NullString{String: "field", Valid: true},
				DataTypeValue:   sql.NullString{String: "integer", Valid: true},
				ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
				PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
				UniqueValue:     sql.NullBool{Bool: false, Valid: true},
				NullableValue:   sql.NullBool{Bool: false, Valid: true},
			}},
		}, {
			"normal index",
			[]string{
				"CREATE TABLE `test-c` (`field` integer NOT NULL)",
				"CREATE INDEX `idx_uq` ON `test-c`(`field`)",
			},
			1,
			[]migrator.ColumnType{{
				NameValue:       sql.NullString{String: "field", Valid: true},
				DataTypeValue:   sql.NullString{String: "integer", Valid: true},
				ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
				PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
				UniqueValue:     sql.NullBool{Bool: false, Valid: true},
				NullableValue:   sql.NullBool{Bool: false, Valid: true},
			}},
		}, {
			"unique constraint",
			[]string{
				"CREATE TABLE `unique_struct` (`name` text,CONSTRAINT `uni_unique_struct_name` UNIQUE (`name`))",
			},
			2,
			[]migrator.ColumnType{{
				NameValue:       sql.NullString{String: "name", Valid: true},
				DataTypeValue:   sql.NullString{String: "text", Valid: true},
				ColumnTypeValue: sql.NullString{String: "text", Valid: true},
				PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
				UniqueValue:     sql.NullBool{Bool: true, Valid: true},
				NullableValue:   sql.NullBool{Bool: true, Valid: true},
			}},
		},
		{
			"non-unique index",
			[]string{
				"CREATE TABLE `test-c` (`field` integer NOT NULL)",
				"CREATE INDEX `idx_uq` ON `test-b`(`field`) WHERE field = 0",
			},
			1,
			[]migrator.ColumnType{
				{
					NameValue:       sql.NullString{String: "field", Valid: true},
					DataTypeValue:   sql.NullString{String: "integer", Valid: true},
					ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
					PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
					UniqueValue:     sql.NullBool{Bool: false, Valid: true},
					NullableValue:   sql.NullBool{Bool: false, Valid: true},
				},
			},
		},
		{
			"index with \n from .schema sqlite",
			[]string{
				"CREATE TABLE `test-d` (`field` integer NOT NULL)",
				"CREATE INDEX `idx_uq`\n    ON `test-b`(`field`) WHERE field = 0",
			},
			1,
			[]migrator.ColumnType{
				{
					NameValue:       sql.NullString{String: "field", Valid: true},
					DataTypeValue:   sql.NullString{String: "integer", Valid: true},
					ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
					PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
					UniqueValue:     sql.NullBool{Bool: false, Valid: true},
					NullableValue:   sql.NullBool{Bool: false, Valid: true},
				},
			},
		},
		{
			"index with \n from .schema sqlite",
			[]string{
				"CREATE TABLE `test-d` (`field` integer NOT NULL)",
				"CREATE INDEX `idx_uq`\n    ON `test-b`(`field`) WHERE field = 0",
			},
			1,
			[]migrator.ColumnType{
				{
					NameValue:       sql.NullString{String: "field", Valid: true},
					DataTypeValue:   sql.NullString{String: "integer", Valid: true},
					ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
					PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
					UniqueValue:     sql.NullBool{Bool: false, Valid: true},
					NullableValue:   sql.NullBool{Bool: false, Valid: true},
				},
			},
		},
	}

	for _, p := range params {
		t.Run(p.name, func(t *testing.T) {
			ddl, err := parseDDL(p.sql...)

			if err != nil {
				panic(err.Error())
			}

			tests.AssertEqual(t, p.sql[0], ddl.compile())
			if len(ddl.fields) != p.nFields {
				t.Fatalf("fields length doesn't match: expect: %v, got %v", p.nFields, len(ddl.fields))
			}
			tests.AssertEqual(t, ddl.columns, p.columns)
		})
	}
}

func TestParseDDL_Whitespaces(t *testing.T) {
	testColumns := []migrator.ColumnType{
		{
			NameValue:         sql.NullString{String: "id", Valid: true},
			DataTypeValue:     sql.NullString{String: "integer", Valid: true},
			ColumnTypeValue:   sql.NullString{String: "integer", Valid: true},
			NullableValue:     sql.NullBool{Bool: true, Valid: true},
			DefaultValueValue: sql.NullString{Valid: false},
			UniqueValue:       sql.NullBool{Bool: true, Valid: true},
			PrimaryKeyValue:   sql.NullBool{Bool: true, Valid: true},
		},
		{
			NameValue:         sql.NullString{String: "dark_mode", Valid: true},
			DataTypeValue:     sql.NullString{String: "numeric", Valid: true},
			ColumnTypeValue:   sql.NullString{String: "numeric", Valid: true},
			NullableValue:     sql.NullBool{Bool: true, Valid: true},
			DefaultValueValue: sql.NullString{String: "true", Valid: true},
			UniqueValue:       sql.NullBool{Bool: false, Valid: true},
			PrimaryKeyValue:   sql.NullBool{Bool: false, Valid: true},
		},
	}

	params := []struct {
		name    string
		sql     []string
		nFields int
		columns []migrator.ColumnType
	}{
		{
			"with_newline",
			[]string{"CREATE TABLE `users`\n(\nid integer primary key unique,\ndark_mode numeric DEFAULT true)"},
			2,
			testColumns,
		},
		{
			"with_newline_2",
			[]string{"CREATE TABLE `users` (\n\nid integer primary key unique,\ndark_mode numeric DEFAULT true)"},
			2,
			testColumns,
		},
		{
			"with_missing_space",
			[]string{"CREATE TABLE `users`(id integer primary key unique, dark_mode numeric DEFAULT true)"},
			2,
			testColumns,
		},
		{
			"with_many_spaces",
			[]string{"CREATE TABLE `users`       (id    integer   primary key unique,     dark_mode    numeric DEFAULT true)"},
			2,
			testColumns,
		},
	}
	for _, p := range params {
		t.Run(p.name, func(t *testing.T) {
			ddl, err := parseDDL(p.sql...)

			if err != nil {
				panic(err.Error())
			}

			if len(ddl.fields) != p.nFields {
				t.Fatalf("fields length doesn't match: expect: %v, got %v", p.nFields, len(ddl.fields))
			}
			tests.AssertEqual(t, ddl.columns, p.columns)
		})
	}
}

func TestParseDDL_error(t *testing.T) {
	params := []struct {
		name string
		sql  string
	}{
		{"invalid_cmd", "CREATE TABLE"},
		{"unbalanced_brackets", "CREATE TABLE test (ID int NOT NULL,Name varchar(255)"},
		{"unbalanced_brackets2", "CREATE TABLE test (ID int NOT NULL,Name varchar(255)))"},
	}

	for _, p := range params {
		t.Run(p.name, func(t *testing.T) {
			_, err := parseDDL(p.sql)
			if err == nil {
				t.Fail()
			}
		})
	}
}

func TestAddConstraint(t *testing.T) {
	params := []struct {
		name   string
		fields []string
		cName  string
		sql    string
		expect []string
	}{
		{
			name:   "add_new",
			fields: []string{"`id` integer NOT NULL"},
			cName:  "fk_users_notes",
			sql:    "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))",
			expect: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
		},
		{
			name:   "update",
			fields: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
			cName:  "fk_users_notes",
			sql:    "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)) ON UPDATE CASCADE ON DELETE CASCADE",
			expect: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)) ON UPDATE CASCADE ON DELETE CASCADE"},
		},
		{
			name:   "add_check",
			fields: []string{"`id` integer NOT NULL"},
			cName:  "name_checker",
			sql:    "CONSTRAINT `name_checker` CHECK (`name` <> 'jinzhu')",
			expect: []string{"`id` integer NOT NULL", "CONSTRAINT `name_checker` CHECK (`name` <> 'jinzhu')"},
		},
		{
			name:   "update_check",
			fields: []string{"`id` integer NOT NULL", "CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')"},
			cName:  "name_checker",
			sql:    "CONSTRAINT `name_checker` CHECK (`name` <> 'jinzhu')",
			expect: []string{"`id` integer NOT NULL", "CONSTRAINT `name_checker` CHECK (`name` <> 'jinzhu')"},
		},
	}

	for _, p := range params {
		t.Run(p.name, func(t *testing.T) {
			testDDL := ddl{fields: p.fields}

			testDDL.addConstraint(p.cName, p.sql)
			tests.AssertEqual(t, p.expect, testDDL.fields)
		})
	}
}

func TestRemoveConstraint(t *testing.T) {
	params := []struct {
		name    string
		fields  []string
		cName   string
		success bool
		expect  []string
	}{
		{
			name:    "fk",
			fields:  []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
			cName:   "fk_users_notes",
			success: true,
			expect:  []string{"`id` integer NOT NULL"},
		},
		{
			name:    "check",
			fields:  []string{"CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')", "`id` integer NOT NULL"},
			cName:   "name_checker",
			success: true,
			expect:  []string{"`id` integer NOT NULL"},
		},
		{
			name:    "none",
			fields:  []string{"CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')", "`id` integer NOT NULL"},
			cName:   "nothing",
			success: false,
			expect:  []string{"CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')", "`id` integer NOT NULL"},
		},
	}

	for _, p := range params {
		t.Run(p.name, func(t *testing.T) {
			testDDL := ddl{fields: p.fields}

			success := testDDL.removeConstraint(p.cName)

			tests.AssertEqual(t, p.success, success)
			tests.AssertEqual(t, p.expect, testDDL.fields)
		})
	}
}

func TestGetColumns(t *testing.T) {
	params := []struct {
		name    string
		ddl     string
		columns []string
	}{
		{
			name:    "with_fk",
			ddl:     "CREATE TABLE `notes` (`id` integer NOT NULL,`text` varchar(500),`user_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))",
			columns: []string{"`id`", "`text`", "`user_id`"},
		},
		{
			name:    "with_check",
			ddl:     "CREATE TABLE Persons (ID int NOT NULL,LastName varchar(255) NOT NULL,FirstName varchar(255),Age int,CHECK (Age>=18),CHECK (FirstName!='John'))",
			columns: []string{"`ID`", "`LastName`", "`FirstName`", "`Age`"},
		},
		{
			name:    "with_escaped_quote",
			ddl:     "CREATE TABLE Persons (ID int NOT NULL,LastName varchar(255) NOT NULL DEFAULT \"\",FirstName varchar(255))",
			columns: []string{"`ID`", "`LastName`", "`FirstName`"},
		},
		{
			name:    "with_generated_column",
			ddl:     "CREATE TABLE Persons (ID int NOT NULL,LastName varchar(255) NOT NULL,FirstName varchar(255),FullName varchar(255) GENERATED ALWAYS AS (FirstName || ' ' || LastName))",
			columns: []string{"`ID`", "`LastName`", "`FirstName`"},
		},
		{
			name: "with_new_line",
			ddl: `CREATE TABLE "tb_sys_role_menu__temp" (
  "id" integer  PRIMARY KEY AUTOINCREMENT,
  "created_at" datetime NOT NULL,
  "updated_at" datetime NOT NULL,
  "created_by" integer NOT NULL DEFAULT 0,
  "updated_by" integer NOT NULL DEFAULT 0,
  "role_id" integer NOT NULL,
  "menu_id" bigint NOT NULL
)`,
			columns: []string{"`id`", "`created_at`", "`updated_at`", "`created_by`", "`updated_by`", "`role_id`", "`menu_id`"},
		},
	}

	for _, p := range params {
		t.Run(p.name, func(t *testing.T) {
			testDDL, err := parseDDL(p.ddl)
			if err != nil {
				panic(err.Error())
			}

			cols := testDDL.getColumns()

			tests.AssertEqual(t, p.columns, cols)
		})
	}
}
