package core_test

import (
	"encoding/json/v2"
	"fmt"
	"slices"
	"testing"

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/tests"
)

func ensureNoTempViews(app core.App, t *testing.T) {
	var total int

	err := app.DB().Select("count(*)").
		From("sqlite_schema").
		AndWhere(dbx.HashExp{"type": "view"}).
		AndWhere(dbx.NewExp(`[[name]] LIKE '%\_temp\_%' ESCAPE '\'`)).
		Limit(1).
		Row(&total)
	if err != nil {
		t.Fatalf("Failed to check for temp views: %v", err)
	}

	if total > 0 {
		t.Fatalf("Expected all temp views to be deleted, got %d", total)
	}
}

func TestDeleteView(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	scenarios := []struct {
		viewName    string
		expectError bool
	}{
		{"", true},
		{"demo1", true},    // not a view table
		{"missing", false}, // missing or already deleted
		{"view1", false},   // existing
		{"VieW1", false},   // view names are case insensitives
	}

	for i, s := range scenarios {
		err := app.DeleteView(s.viewName)

		hasErr := err != nil
		if hasErr != s.expectError {
			t.Errorf("[%d - %q] Expected hasErr %v, got %v (%v)", i, s.viewName, s.expectError, hasErr, err)
		}
	}

	ensureNoTempViews(app, t)
}

func TestSaveView(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	scenarios := []struct {
		scenarioName  string
		viewName      string
		query         string
		expectError   bool
		expectColumns []string
	}{
		{
			"empty name and query",
			"",
			"",
			true,
			nil,
		},
		{
			"empty name",
			"",
			"select * from " + core.CollectionNameSuperusers,
			true,
			nil,
		},
		{
			"empty query",
			"123Test",
			"",
			true,
			nil,
		},
		{
			"invalid query",
			"123Test",
			"123 456",
			true,
			nil,
		},
		{
			"missing table",
			"123Test",
			"select id from missing",
			true,
			nil,
		},
		{
			"non select query",
			"123Test",
			"drop table " + core.CollectionNameSuperusers,
			true,
			nil,
		},
		{
			"multiple select queries",
			"123Test",
			"select *, count(id) as c  from " + core.CollectionNameSuperusers + "; select * from demo1;",
			true,
			nil,
		},
		{
			"try to break the parent parenthesis",
			"123Test",
			"select *, count(id) as c  from `" + core.CollectionNameSuperusers + "`)",
			true,
			nil,
		},
		{
			"simple select query (+ trimmed semicolon)",
			"123Test",
			";select *, count(id) as c  from " + core.CollectionNameSuperusers + ";",
			false,
			[]string{
				"id", "created", "updated",
				"password", "tokenKey", "email",
				"emailVisibility", "verified",
				"c",
			},
		},
		{
			"update old view with new query",
			"123Test",
			"select 1 as test from " + core.CollectionNameSuperusers,
			false,
			[]string{"test"},
		},
	}

	for _, s := range scenarios {
		t.Run(s.scenarioName, func(t *testing.T) {
			err := app.SaveView(s.viewName, s.query)

			hasErr := err != nil
			if hasErr != s.expectError {
				t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
			}

			if hasErr {
				return
			}

			infoRows, err := app.TableInfo(s.viewName)
			if err != nil {
				t.Fatalf("Failed to fetch table info for %s: %v", s.viewName, err)
			}

			if len(s.expectColumns) != len(infoRows) {
				t.Fatalf("Expected %d columns, got %d", len(s.expectColumns), len(infoRows))
			}

			for _, row := range infoRows {
				if !slices.Contains(s.expectColumns, row.Name) {
					t.Fatalf("Missing %q column in %v", row.Name, s.expectColumns)
				}
			}
		})
	}

	ensureNoTempViews(app, t)
}

func TestCreateViewFieldsWithDiscardedNestedTransaction(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	app.RunInTransaction(func(txApp core.App) error {
		_, err := txApp.CreateViewFields("select id from missing")
		if err == nil {
			t.Fatal("Expected error, got nil")
		}

		return nil
	})

	ensureNoTempViews(app, t)
}

func TestCreateViewFields(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	scenarios := []struct {
		name         string
		query        string
		expectError  bool
		expectFields map[string]string // name-type pairs
	}{
		{
			"empty query",
			"",
			true,
			nil,
		},
		{
			"invalid query",
			"test 123456",
			true,
			nil,
		},
		{
			"missing table",
			"select id from missing",
			true,
			nil,
		},
		{
			"query with wildcard column",
			"select a.id, a.* from demo1 a",
			true,
			nil,
		},
		{
			"wrapped query with wildcard column",
			"select * from (select 1 as id)",
			true,
			nil,
		},
		{
			"query without id",
			"select text, url, created, updated from demo1",
			true,
			nil,
		},
		{
			"query with comments",
			`
				select
				-- test single line
				demo1.id,
				demo1.text,
				/* multi
				 * line comment block */
				demo1.url, demo1.created, demo1.updated from/* inline comment block with no spaces between the identifiers */demo1
				-- comment before join
				join demo2 ON (
					-- comment inside join
					demo2.id = demo1.id
				)
				-- comment before where
				where (
					-- comment inside where
					demo2.id = demo1.id
				)
			`,
			false,
			map[string]string{
				"id":      core.FieldTypeText,
				"text":    core.FieldTypeText,
				"url":     core.FieldTypeURL,
				"created": core.FieldTypeAutodate,
				"updated": core.FieldTypeAutodate,
			},
		},
		{
			"query with all fields and quoted identifiers",
			`
				select
					"id",
					"created",
					"updated",
					[text],
					` + "`bool`" + `,
					"url",
					"select_one",
					"select_many",
					"file_one",
					"demo1"."file_many",
					` + "`demo1`." + "`number`" + ` number_alias,
					"email",
					"datetime",
					"json",
					"rel_one",
					"rel_many",
					'single_quoted_custom_literal' as 'single_quoted_column'
				from demo1
			`,
			false,
			map[string]string{
				"id":                   core.FieldTypeText,
				"created":              core.FieldTypeAutodate,
				"updated":              core.FieldTypeAutodate,
				"text":                 core.FieldTypeText,
				"bool":                 core.FieldTypeBool,
				"url":                  core.FieldTypeURL,
				"select_one":           core.FieldTypeSelect,
				"select_many":          core.FieldTypeSelect,
				"file_one":             core.FieldTypeFile,
				"file_many":            core.FieldTypeFile,
				"number_alias":         core.FieldTypeNumber,
				"email":                core.FieldTypeEmail,
				"datetime":             core.FieldTypeDate,
				"json":                 core.FieldTypeJSON,
				"rel_one":              core.FieldTypeRelation,
				"rel_many":             core.FieldTypeRelation,
				"single_quoted_column": core.FieldTypeJSON,
			},
		},
		{
			"query with indirect relations fields",
			"select a.id, b.id as bid, b.created from demo1 as a left join demo2 b",
			false,
			map[string]string{
				"id":      core.FieldTypeText,
				"bid":     core.FieldTypeRelation,
				"created": core.FieldTypeAutodate,
			},
		},
		{
			"query with multiple froms, joins and style of aliases",
			`
				select
					a.id as id,
					b.id as bid,
					lj.id cid,
					ij.id as did,
					a.bool,
					` + core.CollectionNameSuperusers + `.id as eid,
					` + core.CollectionNameSuperusers + `.email
				from demo1 a, demo2 as b
				left join demo3 lj on lj.id = 123
				inner join demo4 as ij on ij.id = 123
				join ` + core.CollectionNameSuperusers + `
				where 1=1
				group by a.id
				limit 10
			`,
			false,
			map[string]string{
				"id":    core.FieldTypeText,
				"bid":   core.FieldTypeRelation,
				"cid":   core.FieldTypeRelation,
				"did":   core.FieldTypeRelation,
				"bool":  core.FieldTypeBool,
				"eid":   core.FieldTypeRelation,
				"email": core.FieldTypeEmail,
			},
		},
		{
			"query with casts",
			`select
				a.id,
				count(a.id) count,
				cast(a.id as int) cast_int,
				cast(a.id as integer) cast_integer,
				cast(a.id as real) cast_real,
				cast(a.id as decimal) cast_decimal,
				cast(a.id as numeric) cast_numeric,
				cast(a.id as text) cast_text,
				cast(a.id as bool) cast_bool,
				cast(a.id as boolean) cast_boolean,
				avg(a.id) avg,
				sum(a.id) sum,
				total(a.id) total,
				min(a.id) min,
				max(a.id) max
			from demo1 a`,
			false,
			map[string]string{
				"id":           core.FieldTypeText,
				"count":        core.FieldTypeNumber,
				"total":        core.FieldTypeNumber,
				"cast_int":     core.FieldTypeNumber,
				"cast_integer": core.FieldTypeNumber,
				"cast_real":    core.FieldTypeNumber,
				"cast_decimal": core.FieldTypeNumber,
				"cast_numeric": core.FieldTypeNumber,
				"cast_text":    core.FieldTypeText,
				"cast_bool":    core.FieldTypeBool,
				"cast_boolean": core.FieldTypeBool,
				// json because they are nullable
				"sum": core.FieldTypeJSON,
				"avg": core.FieldTypeJSON,
				"min": core.FieldTypeJSON,
				"max": core.FieldTypeJSON,
			},
		},
		{
			"query with multiline cast",
			`select
				id,
				cast(
					(
						case
							when count(a.id) = 1 then 21
							when count(a.id) = 2 then 18
							else 0
						end
					) as int
				) as cast_int
			from demo1 a`,
			false,
			map[string]string{
				"id":       core.FieldTypeText,
				"cast_int": core.FieldTypeNumber,
			},
		},
		{
			"query with case-insensitive and extra-spaced cast",
			`select
				id,
				CaSt( a.id  aS iNt ) as cast_int
			from demo1 a`,
			false,
			map[string]string{
				"id":       core.FieldTypeText,
				"cast_int": core.FieldTypeNumber,
			},
		},
		{
			"query with reserved auth collection fields",
			`
				select
					a.id,
					a.username,
					a.email,
					a.emailVisibility,
					a.verified,
					demo1.id relid
				from users a
				left join demo1
			`,
			false,
			map[string]string{
				"id":              core.FieldTypeText,
				"username":        core.FieldTypeText,
				"email":           core.FieldTypeEmail,
				"emailVisibility": core.FieldTypeBool,
				"verified":        core.FieldTypeBool,
				"relid":           core.FieldTypeRelation,
			},
		},
		{
			"query with unknown fields and aliases",
			`select
				id,
				id as id2,
				text as text_alias,
				url as url_alias,
				"demo1"."bool" as bool_alias,
				number as number_alias,
				created created_alias,
				updated updated_alias,
				123 as custom
			from demo1`,
			false,
			map[string]string{
				"id":            core.FieldTypeText,
				"id2":           core.FieldTypeRelation,
				"text_alias":    core.FieldTypeText,
				"url_alias":     core.FieldTypeURL,
				"bool_alias":    core.FieldTypeBool,
				"number_alias":  core.FieldTypeNumber,
				"created_alias": core.FieldTypeAutodate,
				"updated_alias": core.FieldTypeAutodate,
				"custom":        core.FieldTypeJSON,
			},
		},
		{
			"query with distinct and reordered id column",
			`select distinct
				id as id2,
				id,
				123 as custom
			from demo1`,
			false,
			map[string]string{
				"id2":    core.FieldTypeRelation,
				"id":     core.FieldTypeText,
				"custom": core.FieldTypeJSON,
			},
		},
		{
			"query with aliasing the same field multiple times",
			`select
				a.id as id,
				a.text as alias1,
				a.text as alias2,
				b.text as alias3,
				b.text as alias4
			from demo1 a
			left join demo1 as b`,
			false,
			map[string]string{
				"id":     core.FieldTypeText,
				"alias1": core.FieldTypeText,
				"alias2": core.FieldTypeText,
				"alias3": core.FieldTypeText,
				"alias4": core.FieldTypeText,
			},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result, err := app.CreateViewFields(s.query)

			hasErr := err != nil
			if hasErr != s.expectError {
				t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
			}

			if hasErr {
				return
			}

			if len(s.expectFields) != len(result) {
				serialized, _ := json.Marshal(result, json.Deterministic(true))
				t.Fatalf("Expected %d fields, got %d: \n%s", len(s.expectFields), len(result), serialized)
			}

			for name, typ := range s.expectFields {
				field := result.GetByName(name)

				if field == nil {
					t.Fatalf("Expected to find field %s, got nil", name)
				}

				if field.Type() != typ {
					t.Fatalf("Expected field %s to be %q, got %q", name, typ, field.Type())
				}
			}
		})
	}

	ensureNoTempViews(app, t)
}

func TestCreateViewFieldsWithNumberOnlyInt(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	sql := `select
		a.id,
		count(a.id) count,
		total(a.id) total,
		cast(a.id as int) cast_int,
		cast(a.id as integer) cast_integer,
		cast(a.id as real) cast_real,
		cast(a.id as decimal) cast_decimal,
		cast(a.id as numeric) cast_numeric
	from demo1 a`

	result, err := app.CreateViewFields(sql)
	if err != nil {
		t.Fatal(err)
	}

	onlyInts := map[string]bool{
		"count":        true,
		"total":        false,
		"cast_int":     true,
		"cast_integer": true,
		"cast_real":    false,
		"cast_decimal": false,
		"cast_numeric": false,
	}

	totalExpected := len(onlyInts) + 1
	if total := len(result); total != totalExpected {
		t.Fatalf("Expected %d, got %d", totalExpected, total)
	}

	for _, f := range result {
		if f.GetName() == "id" {
			continue
		}

		t.Run(f.GetName(), func(t *testing.T) {
			nf, ok := f.(*core.NumberField)
			if !ok {
				t.Fatalf("Expected *core.NumberField, got %v", f)
			}

			if nf.OnlyInt != onlyInts[nf.Name] {
				t.Fatalf("Expected OnlyInt %v, got %v", onlyInts[nf.Name], nf.OnlyInt)
			}
		})
	}
}

func TestFindRecordByViewFile(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	prevCollection, err := app.FindCollectionByNameOrId("demo1")
	if err != nil {
		t.Fatal(err)
	}

	totalLevels := 6

	// create collection view mocks
	fileOneAlias := "file_one one0"
	fileManyAlias := "file_many many0"
	mockCollections := make([]*core.Collection, 0, totalLevels)
	for i := 0; i <= totalLevels; i++ {
		view := new(core.Collection)
		view.Type = core.CollectionTypeView
		view.Name = fmt.Sprintf("_test_view%d", i)
		view.ViewQuery = fmt.Sprintf(
			"select id, %s, %s from %s",
			fileOneAlias,
			fileManyAlias,
			prevCollection.Name,
		)

		// save view
		if err := app.Save(view); err != nil {
			t.Fatalf("Failed to save view%d: %v", i, err)
		}

		mockCollections = append(mockCollections, view)
		prevCollection = view
		fileOneAlias = fmt.Sprintf("one%d one%d", i, i+1)
		fileManyAlias = fmt.Sprintf("many%d many%d", i, i+1)
	}

	fileOneName := "test_d61b33QdDU.txt"
	fileManyName := "test_QZFjKjXchk.txt"
	expectedRecordId := "84nmscqy84lsi1t"

	scenarios := []struct {
		name               string
		collectionNameOrId string
		fileFieldName      string
		filename           string
		expectError        bool
		expectRecordId     string
	}{
		{
			"missing collection",
			"missing",
			"a",
			fileOneName,
			true,
			"",
		},
		{
			"non-view collection",
			"demo1",
			"file_one",
			fileOneName,
			true,
			"",
		},
		{
			"view collection after the max recursion limit",
			mockCollections[totalLevels-1].Name,
			fmt.Sprintf("one%d", totalLevels-1),
			fileOneName,
			true,
			"",
		},
		{
			"first view collection (single file)",
			mockCollections[0].Name,
			"one0",
			fileOneName,
			false,
			expectedRecordId,
		},
		{
			"first view collection (many files)",
			mockCollections[0].Name,
			"many0",
			fileManyName,
			false,
			expectedRecordId,
		},
		{
			"last view collection before the recursion limit (single file)",
			mockCollections[totalLevels-2].Name,
			fmt.Sprintf("one%d", totalLevels-2),
			fileOneName,
			false,
			expectedRecordId,
		},
		{
			"last view collection before the recursion limit (many files)",
			mockCollections[totalLevels-2].Name,
			fmt.Sprintf("many%d", totalLevels-2),
			fileManyName,
			false,
			expectedRecordId,
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			record, err := app.FindRecordByViewFile(
				s.collectionNameOrId,
				s.fileFieldName,
				s.filename,
			)

			hasErr := err != nil
			if hasErr != s.expectError {
				t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
			}

			if hasErr {
				return
			}

			if record.Id != s.expectRecordId {
				t.Fatalf("Expected recordId %q, got %q", s.expectRecordId, record.Id)
			}
		})
	}
}

func TestDryRunView(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	scenarios := []struct {
		name            string
		query           string
		sampleSize      int
		expectError     bool
		expectFields    map[string]string // name-type pairs
		expectSampleIds []string          // record ids of the resulting sample
	}{
		{
			"empty query",
			"",
			10,
			true,
			nil,
			nil,
		},
		{
			"non-select query",
			"CREATE TABLE t1(x INT)",
			10,
			true,
			nil,
			nil,
		},
		{
			"multiple inline select statements",
			"select 'a' as id; select 'b' as id",
			10,
			true,
			nil,
			nil,
		},
		{
			"select with invalid formatted field name",
			"select 'a' as id, count(*)", // missing field alias
			10,
			true,
			nil,
			nil,
		},
		{
			"select resolving to records with missing id",
			"select id from (select 'a' as id UNION ALL select null as id UNION ALL select 'c' as id)",
			10,
			true,
			nil,
			nil,
		},
		{
			"select resolving to records with duplicated ids",
			"select id from (select 'a' as id UNION ALL select 'a' as id UNION ALL select 'c' as id)",
			10,
			true,
			nil,
			nil,
		},
		{
			"no sample size and valid select query but with invalid records result",
			"select id from (select 'a' as id UNION ALL select 'a' as id UNION ALL select 'c' as id)",
			0,
			false, // still "valid" because there is no sample to check
			map[string]string{"id": "text"},
			nil,
		},
		{
			"sample size < total select records",
			"select id from (select 'a' as id UNION ALL select 'b' as id UNION ALL select 'c' as id UNION ALL select 'd' as id)",
			3,
			false,
			map[string]string{"id": "text"},
			[]string{"a", "b", "c"},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result, err := app.DryRunView(s.query, s.sampleSize)

			hasErr := err != nil
			if hasErr != s.expectError {
				t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
			}

			if hasErr {
				return
			}

			// check fields
			// ---
			if len(s.expectFields) != len(result.Fields) {
				serialized, _ := json.Marshal(result.Fields, json.Deterministic(true))
				t.Fatalf("Expected %d fields, got %d: \n%s", len(s.expectFields), len(result.Fields), serialized)
			}
			for name, typ := range s.expectFields {
				field := result.Fields.GetByName(name)
				if field == nil {
					t.Fatalf("Expected to find field %s, got nil", name)
				}

				if field.Type() != typ {
					t.Fatalf("Expected field %s to be %q, got %q", name, typ, field.Type())
				}
			}

			// check sample ids
			// ---
			if len(s.expectSampleIds) != len(result.Sample) {
				t.Fatalf("Expected %d sample records, got %d", len(s.expectSampleIds), len(result.Sample))
			}
			for i, r := range result.Sample {
				if s.expectSampleIds[i] != r.Id {
					t.Fatalf("Expected sample record id %q, got %q at %d", s.expectSampleIds[i], r.Id, i)
				}
			}
		})
	}

	ensureNoTempViews(app, t)
}
