package schema_test

import (
	"testing"

	"github.com/pocketbase/pocketbase/models/schema"
)

func TestNewSchemaAndFields(t *testing.T) {
	testSchema := schema.NewSchema(
		&schema.SchemaField{Id: "id1", Name: "test1"},
		&schema.SchemaField{Name: "test2"},
		&schema.SchemaField{Id: "id1", Name: "test1_new"}, // should replace the original id1 field
	)

	fields := testSchema.Fields()

	if len(fields) != 2 {
		t.Fatalf("Expected 2 fields, got %d (%v)", len(fields), fields)
	}

	for _, f := range fields {
		if f.Id == "" {
			t.Fatalf("Expected field id to be set, found empty id for field %v", f)
		}
	}

	if fields[0].Name != "test1_new" {
		t.Fatalf("Expected field with name test1_new, got %s", fields[0].Name)
	}

	if fields[1].Name != "test2" {
		t.Fatalf("Expected field with name test2, got %s", fields[1].Name)
	}
}

func TestSchemaInitFieldsOptions(t *testing.T) {
	f0 := &schema.SchemaField{Name: "test1", Type: "unknown"}
	schema0 := schema.NewSchema(f0)

	err0 := schema0.InitFieldsOptions()
	if err0 == nil {
		t.Fatalf("Expected unknown field schema to fail, got nil")
	}

	// ---

	f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
	f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail}
	schema1 := schema.NewSchema(f1, f2)

	err1 := schema1.InitFieldsOptions()
	if err1 != nil {
		t.Fatal(err1)
	}

	if _, ok := f1.Options.(*schema.TextOptions); !ok {
		t.Fatalf("Failed to init f1 options")
	}

	if _, ok := f2.Options.(*schema.EmailOptions); !ok {
		t.Fatalf("Failed to init f2 options")
	}
}

func TestSchemaClone(t *testing.T) {
	f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
	f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail}
	s1 := schema.NewSchema(f1, f2)

	s2, err := s1.Clone()
	if err != nil {
		t.Fatal(err)
	}

	s1Encoded, _ := s1.MarshalJSON()
	s2Encoded, _ := s2.MarshalJSON()

	if string(s1Encoded) != string(s2Encoded) {
		t.Fatalf("Expected the cloned schema to be equal, got %v VS\n %v", s1, s2)
	}

	// change in one schema shouldn't result to change in the other
	// (aka. check if it is a deep clone)
	s1.Fields()[0].Name = "test1_update"
	if s2.Fields()[0].Name != "test1" {
		t.Fatalf("Expected s2 field name to not change, got %q", s2.Fields()[0].Name)
	}
}

func TestSchemaAsMap(t *testing.T) {
	f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
	f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail}
	testSchema := schema.NewSchema(f1, f2)

	result := testSchema.AsMap()

	if len(result) != 2 {
		t.Fatalf("Expected 2 map elements, got %d (%v)", len(result), result)
	}

	expectedIndexes := []string{f1.Name, f2.Name}

	for _, index := range expectedIndexes {
		if _, ok := result[index]; !ok {
			t.Fatalf("Missing index %q", index)
		}
	}
}

func TestSchemaGetFieldByName(t *testing.T) {
	f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
	f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeText}
	testSchema := schema.NewSchema(f1, f2)

	// missing field
	result1 := testSchema.GetFieldByName("missing")
	if result1 != nil {
		t.Fatalf("Found unexpected field %v", result1)
	}

	// existing field
	result2 := testSchema.GetFieldByName("test1")
	if result2 == nil || result2.Name != "test1" {
		t.Fatalf("Cannot find field with Name 'test1', got %v ", result2)
	}
}

func TestSchemaGetFieldById(t *testing.T) {
	f1 := &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText}
	f2 := &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText}
	testSchema := schema.NewSchema(f1, f2)

	// missing field id
	result1 := testSchema.GetFieldById("test1")
	if result1 != nil {
		t.Fatalf("Found unexpected field %v", result1)
	}

	// existing field id
	result2 := testSchema.GetFieldById("id2")
	if result2 == nil || result2.Id != "id2" {
		t.Fatalf("Cannot find field with id 'id2', got %v ", result2)
	}
}

func TestSchemaRemoveField(t *testing.T) {
	f1 := &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText}
	f2 := &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText}
	f3 := &schema.SchemaField{Id: "id3", Name: "test3", Type: schema.FieldTypeText}
	testSchema := schema.NewSchema(f1, f2, f3)

	testSchema.RemoveField("id2")
	testSchema.RemoveField("test3") // should do nothing

	expected := []string{"test1", "test3"}

	if len(testSchema.Fields()) != len(expected) {
		t.Fatalf("Expected %d, got %d (%v)", len(expected), len(testSchema.Fields()), testSchema)
	}

	for _, name := range expected {
		if f := testSchema.GetFieldByName(name); f == nil {
			t.Fatalf("Missing field %q", name)
		}
	}
}

func TestSchemaAddField(t *testing.T) {
	f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
	f2 := &schema.SchemaField{Id: "f2Id", Name: "test2", Type: schema.FieldTypeText}
	f3 := &schema.SchemaField{Id: "f3Id", Name: "test3", Type: schema.FieldTypeText}
	testSchema := schema.NewSchema(f1, f2, f3)

	f2New := &schema.SchemaField{Id: "f2Id", Name: "test2_new", Type: schema.FieldTypeEmail}
	f4 := &schema.SchemaField{Name: "test4", Type: schema.FieldTypeUrl}

	testSchema.AddField(f2New)
	testSchema.AddField(f4)

	if len(testSchema.Fields()) != 4 {
		t.Fatalf("Expected %d, got %d (%v)", 4, len(testSchema.Fields()), testSchema)
	}

	// check if each field has id
	for _, f := range testSchema.Fields() {
		if f.Id == "" {
			t.Fatalf("Expected field id to be set, found empty id for field %v", f)
		}
	}

	// check if f2 field was replaced
	if f := testSchema.GetFieldById("f2Id"); f == nil || f.Type != schema.FieldTypeEmail {
		t.Fatalf("Expected f2 field to be replaced, found %v", f)
	}

	// check if f4 was added
	if f := testSchema.GetFieldByName("test4"); f == nil || f.Name != "test4" {
		t.Fatalf("Expected f4 field to be added, found %v", f)
	}
}

func TestSchemaValidate(t *testing.T) {
	// emulate duplicated field ids
	duplicatedIdsSchema := schema.NewSchema(
		&schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText},
		&schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText},
	)
	duplicatedIdsSchema.Fields()[1].Id = "id1" // manually set existing id

	scenarios := []struct {
		schema      schema.Schema
		expectError bool
	}{
		// no fields
		{
			schema.NewSchema(),
			false,
		},
		// duplicated field ids
		{
			duplicatedIdsSchema,
			true,
		},
		// duplicated field names (case insensitive)
		{
			schema.NewSchema(
				&schema.SchemaField{Name: "test", Type: schema.FieldTypeText},
				&schema.SchemaField{Name: "TeSt", Type: schema.FieldTypeText},
			),
			true,
		},
		// failure - base individual fields validation
		{
			schema.NewSchema(
				&schema.SchemaField{Name: "", Type: schema.FieldTypeText},
			),
			true,
		},
		// success - base individual fields validation
		{
			schema.NewSchema(
				&schema.SchemaField{Name: "test", Type: schema.FieldTypeText},
			),
			false,
		},
		// failure - individual field options validation
		{
			schema.NewSchema(
				&schema.SchemaField{Name: "test", Type: schema.FieldTypeFile},
			),
			true,
		},
		// success - individual field options validation
		{
			schema.NewSchema(
				&schema.SchemaField{Name: "test", Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1}},
			),
			false,
		},
	}

	for i, s := range scenarios {
		err := s.schema.Validate()

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

func TestSchemaMarshalJSON(t *testing.T) {
	f1 := &schema.SchemaField{Id: "f1id", Name: "test1", Type: schema.FieldTypeText}
	f2 := &schema.SchemaField{
		Id:      "f2id",
		Name:    "test2",
		Type:    schema.FieldTypeText,
		Options: &schema.TextOptions{Pattern: "test"},
	}
	testSchema := schema.NewSchema(f1, f2)

	result, err := testSchema.MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}

	expected := `[{"system":false,"id":"f1id","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}},{"system":false,"id":"f2id","name":"test2","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`

	if string(result) != expected {
		t.Fatalf("Expected %s, got %s", expected, string(result))
	}
}

func TestSchemaUnmarshalJSON(t *testing.T) {
	encoded := `[{"system":false,"id":"fid1", "name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}},{"system":false,"name":"test2","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`
	testSchema := schema.Schema{}
	testSchema.AddField(&schema.SchemaField{Name: "tempField", Type: schema.FieldTypeUrl})
	err := testSchema.UnmarshalJSON([]byte(encoded))
	if err != nil {
		t.Fatal(err)
	}

	fields := testSchema.Fields()
	if len(fields) != 2 {
		t.Fatalf("Expected 2 fields, found %v", fields)
	}

	f1 := testSchema.GetFieldByName("test1")
	if f1 == nil {
		t.Fatal("Expected to find field 'test1', got nil")
	}
	if f1.Id != "fid1" {
		t.Fatalf("Expected fid1 id, got %s", f1.Id)
	}
	_, ok := f1.Options.(*schema.TextOptions)
	if !ok {
		t.Fatal("'test1' field options are not inited.")
	}

	f2 := testSchema.GetFieldByName("test2")
	if f2 == nil {
		t.Fatal("Expected to find field 'test2', got nil")
	}
	if f2.Id == "" {
		t.Fatal("Expected f2 id to be set, got empty string")
	}
	o2, ok := f2.Options.(*schema.TextOptions)
	if !ok {
		t.Fatal("'test2' field options are not inited.")
	}
	if o2.Pattern != "test" {
		t.Fatalf("Expected pattern to be %q, got %q", "test", o2.Pattern)
	}
}

func TestSchemaValue(t *testing.T) {
	// empty schema
	s1 := schema.Schema{}
	v1, err := s1.Value()
	if err != nil {
		t.Fatal(err)
	}
	if v1 != "[]" {
		t.Fatalf("Expected nil, got %v", v1)
	}

	// schema with fields
	f1 := &schema.SchemaField{Id: "f1id", Name: "test1", Type: schema.FieldTypeText}
	s2 := schema.NewSchema(f1)

	v2, err := s2.Value()
	if err != nil {
		t.Fatal(err)
	}
	expected := `[{"system":false,"id":"f1id","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`

	if v2 != expected {
		t.Fatalf("Expected %v, got %v", expected, v2)
	}
}

func TestSchemaScan(t *testing.T) {
	scenarios := []struct {
		data        any
		expectError bool
		expectJson  string
	}{
		{nil, false, "[]"},
		{"", false, "[]"},
		{[]byte{}, false, "[]"},
		{"[]", false, "[]"},
		{"invalid", true, "[]"},
		{123, true, "[]"},
		// no field type
		{`[{}]`, true, `[]`},
		// unknown field type
		{
			`[{"system":false,"id":"123","name":"test1","type":"unknown","required":false,"presentable":false,"unique":false}]`,
			true,
			`[]`,
		},
		// without options
		{
			`[{"system":false,"id":"123","name":"test1","type":"text","required":false,"presentable":false,"unique":false}]`,
			false,
			`[{"system":false,"id":"123","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`,
		},
		// with options
		{
			`[{"system":false,"id":"123","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`,
			false,
			`[{"system":false,"id":"123","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`,
		},
	}

	for i, s := range scenarios {
		testSchema := schema.Schema{}

		err := testSchema.Scan(s.data)

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

		json, _ := testSchema.MarshalJSON()
		if string(json) != s.expectJson {
			t.Errorf("(%d) Expected json %v, got %v", i, s.expectJson, string(json))
		}
	}
}