package dbutils_test

import (
	"bytes"
	"encoding/json/v2"
	"fmt"
	"strings"
	"testing"

	"github.com/pocketbase/pocketbase/tools/dbutils"
)

func TestParseIndex(t *testing.T) {
	scenarios := []struct {
		index    string
		expected dbutils.Index
	}{
		// invalid
		{
			`invalid`,
			dbutils.Index{},
		},
		// no names
		{
			`create index on ()`,
			dbutils.Index{},
		},
		// invalid index name
		{
			`create index a.b.c on ()`,
			dbutils.Index{},
		},
		// simple (multiple spaces between the table and columns list)
		{
			`create index indexname on tablename   (col1)`,
			dbutils.Index{
				IndexName: "indexname",
				TableName: "tablename",
				Columns: []dbutils.IndexColumn{
					{Name: "col1"},
				},
			},
		},
		// simple (no space between the table and the columns list)
		{
			`create index indexname on tablename(col1)`,
			dbutils.Index{
				IndexName: "indexname",
				TableName: "tablename",
				Columns: []dbutils.IndexColumn{
					{Name: "col1"},
				},
			},
		},
		// all fields
		{
			`CREATE UNIQUE INDEX IF NOT EXISTS "schemaname".[indexname] on 'tablename' (
				col0,
				` + "`" + `col1` + "`" + `,
				json_extract("col2", "$.a") asc,
				"col3" collate NOCASE,
				"col4" collate RTRIM desc
			) where test = 1`,
			dbutils.Index{
				Unique:     true,
				Optional:   true,
				SchemaName: "schemaname",
				IndexName:  "indexname",
				TableName:  "tablename",
				Columns: []dbutils.IndexColumn{
					{Name: "col0"},
					{Name: "col1"},
					{Name: `json_extract("col2", "$.a")`, Sort: "ASC"},
					{Name: `col3`, Collate: "NOCASE"},
					{Name: `col4`, Collate: "RTRIM", Sort: "DESC"},
				},
				Where: "test = 1",
			},
		},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("scenario_%d", i), func(t *testing.T) {
			result := dbutils.ParseIndex(s.index)

			resultRaw, err := json.Marshal(result)
			if err != nil {
				t.Fatalf("Failed to marshalize parse result: %v", err)
			}

			expectedRaw, err := json.Marshal(s.expected)
			if err != nil {
				t.Fatalf("Failed to marshalize expected index: %v", err)
			}

			if !bytes.Equal(resultRaw, expectedRaw) {
				t.Errorf("Expected \n%s \ngot \n%s", expectedRaw, resultRaw)
			}
		})
	}
}

func TestIndexIsValid(t *testing.T) {
	scenarios := []struct {
		name     string
		index    dbutils.Index
		expected bool
	}{
		{
			"empty",
			dbutils.Index{},
			false,
		},
		{
			"no index name",
			dbutils.Index{
				TableName: "table",
				Columns:   []dbutils.IndexColumn{{Name: "col"}},
			},
			false,
		},
		{
			"no table name",
			dbutils.Index{
				IndexName: "index",
				Columns:   []dbutils.IndexColumn{{Name: "col"}},
			},
			false,
		},
		{
			"no columns",
			dbutils.Index{
				IndexName: "index",
				TableName: "table",
			},
			false,
		},
		{
			"min valid",
			dbutils.Index{
				IndexName: "index",
				TableName: "table",
				Columns:   []dbutils.IndexColumn{{Name: "col"}},
			},
			true,
		},
		{
			"all fields",
			dbutils.Index{
				Optional:   true,
				Unique:     true,
				SchemaName: "schema",
				IndexName:  "index",
				TableName:  "table",
				Columns:    []dbutils.IndexColumn{{Name: "col"}},
				Where:      "test = 1 OR test = 2",
			},
			true,
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.index.IsValid()
			if result != s.expected {
				t.Fatalf("Expected %v, got %v", s.expected, result)
			}
		})
	}
}

func TestIndexBuild(t *testing.T) {
	scenarios := []struct {
		name     string
		index    dbutils.Index
		expected string
	}{
		{
			"empty",
			dbutils.Index{},
			"",
		},
		{
			"no index name",
			dbutils.Index{
				TableName: "table",
				Columns:   []dbutils.IndexColumn{{Name: "col"}},
			},
			"",
		},
		{
			"no table name",
			dbutils.Index{
				IndexName: "index",
				Columns:   []dbutils.IndexColumn{{Name: "col"}},
			},
			"",
		},
		{
			"no columns",
			dbutils.Index{
				IndexName: "index",
				TableName: "table",
			},
			"",
		},
		{
			"min valid",
			dbutils.Index{
				IndexName: "index",
				TableName: "table",
				Columns:   []dbutils.IndexColumn{{Name: "col"}},
			},
			"CREATE INDEX `index` ON `table` (`col`)",
		},
		{
			"all fields",
			dbutils.Index{
				Optional:   true,
				Unique:     true,
				SchemaName: "schema",
				IndexName:  "index",
				TableName:  "table",
				Columns: []dbutils.IndexColumn{
					{Name: "col1", Collate: "NOCASE", Sort: "asc"},
					{Name: "col2", Sort: "desc"},
					{Name: `json_extract("col3", "$.a")`, Collate: "NOCASE"},
				},
				Where: "test = 1 OR test = 2",
			},
			"CREATE UNIQUE INDEX IF NOT EXISTS `schema`.`index` ON `table` (\n  `col1` COLLATE NOCASE ASC,\n  `col2` DESC,\n  " + `json_extract("col3", "$.a")` + " COLLATE NOCASE\n) WHERE test = 1 OR test = 2",
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.index.Build()
			if result != s.expected {
				t.Fatalf("Expected \n%v \ngot \n%v", s.expected, result)
			}
		})
	}
}

func TestHasSingleColumnUniqueIndex(t *testing.T) {
	scenarios := []struct {
		name     string
		column   string
		indexes  []string
		expected bool
	}{
		{
			"empty indexes",
			"test",
			nil,
			false,
		},
		{
			"empty column",
			"",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test`)",
			},
			false,
		},
		{
			"mismatched column",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test2`)",
			},
			false,
		},
		{
			"non unique index",
			"test",
			[]string{
				"CREATE INDEX `index1` ON `example` (`test`)",
			},
			false,
		},
		{
			"matching columnd and unique index",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test`)",
			},
			true,
		},
		{
			"multiple columns",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test`, `test2`)",
			},
			false,
		},
		{
			"multiple indexes",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test`, `test2`)",
				"CREATE UNIQUE INDEX `index2` ON `example` (`test`)",
			},
			true,
		},
		{
			"partial unique index",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index` ON `example` (`test`) where test != ''",
			},
			true,
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := dbutils.HasSingleColumnUniqueIndex(s.column, s.indexes)
			if result != s.expected {
				t.Fatalf("Expected %v got %v", s.expected, result)
			}
		})
	}
}

func TestFindSingleColumnUniqueIndex(t *testing.T) {
	scenarios := []struct {
		name     string
		column   string
		indexes  []string
		expected bool
	}{
		{
			"empty indexes",
			"test",
			nil,
			false,
		},
		{
			"empty column",
			"",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test`)",
			},
			false,
		},
		{
			"mismatched column",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test2`)",
			},
			false,
		},
		{
			"non unique index",
			"test",
			[]string{
				"CREATE INDEX `index1` ON `example` (`test`)",
			},
			false,
		},
		{
			"matching columnd and unique index",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test`)",
			},
			true,
		},
		{
			"multiple columns",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test`, `test2`)",
			},
			false,
		},
		{
			"multiple indexes",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index1` ON `example` (`test`, `test2`)",
				"CREATE UNIQUE INDEX `index2` ON `example` (`test`)",
			},
			true,
		},
		{
			"partial unique index",
			"test",
			[]string{
				"CREATE UNIQUE INDEX `index` ON `example` (`test`) where test != ''",
			},
			true,
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			index, exists := dbutils.FindSingleColumnUniqueIndex(s.indexes, s.column)
			if exists != s.expected {
				t.Fatalf("Expected exists %v got %v", s.expected, exists)
			}

			if !exists && len(index.Columns) > 0 {
				t.Fatal("Expected index.Columns to be empty")
			}

			if exists && !strings.EqualFold(index.Columns[0].Name, s.column) {
				t.Fatalf("Expected to find column %q in %v", s.column, index)
			}
		})
	}
}
