package tests_test

import (
	"database/sql/driver"
	"encoding/json"
	"errors"
	"reflect"
	"testing"
	"time"

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

func TestEmbeddedStruct(t *testing.T) {
	type ReadOnly struct {
		ReadOnly *bool
	}

	type BasePost struct {
		Id    int64
		Title string
		URL   string
		ReadOnly
	}

	type Author struct {
		ID    string
		Name  string
		Email string
	}

	type HNPost struct {
		BasePost
		Author  `gorm:"EmbeddedPrefix:user_"` // Embedded struct
		Upvotes int32
	}

	type EngadgetPost struct {
		BasePost BasePost `gorm:"Embedded"`
		Author   *Author  `gorm:"Embedded;EmbeddedPrefix:author_"` // Embedded struct
		ImageUrl string
	}

	DB.Migrator().DropTable(&HNPost{}, &EngadgetPost{})
	if err := DB.Migrator().AutoMigrate(&HNPost{}, &EngadgetPost{}); err != nil {
		t.Fatalf("failed to auto migrate, got error: %v", err)
	}

	for _, name := range []string{"author_id", "author_name", "author_email"} {
		if !DB.Migrator().HasColumn(&EngadgetPost{}, name) {
			t.Errorf("should has prefixed column %v", name)
		}
	}

	stmt := gorm.Statement{DB: DB}
	if err := stmt.Parse(&EngadgetPost{}); err != nil {
		t.Fatalf("failed to parse embedded struct")
	} else if len(stmt.Schema.PrimaryFields) != 1 {
		t.Errorf("should have only one primary field with embedded struct, but got %v", len(stmt.Schema.PrimaryFields))
	}

	for _, name := range []string{"user_id", "user_name", "user_email"} {
		if !DB.Migrator().HasColumn(&HNPost{}, name) {
			t.Errorf("should has prefixed column %v", name)
		}
	}

	// save embedded struct
	DB.Save(&HNPost{BasePost: BasePost{Title: "news"}})
	DB.Save(&HNPost{BasePost: BasePost{Title: "hn_news"}})
	var news HNPost
	if err := DB.First(&news, "title = ?", "hn_news").Error; err != nil {
		t.Errorf("no error should happen when query with embedded struct, but got %v", err)
	} else if news.Title != "hn_news" {
		t.Errorf("embedded struct's value should be scanned correctly")
	}

	DB.Save(&EngadgetPost{BasePost: BasePost{Title: "engadget_news"}, Author: &Author{Name: "Edward"}})
	DB.Save(&EngadgetPost{BasePost: BasePost{Title: "engadget_article"}, Author: &Author{Name: "George"}})
	var egNews EngadgetPost
	if err := DB.First(&egNews, "title = ?", "engadget_news").Error; err != nil {
		t.Errorf("no error should happen when query with embedded struct, but got %v", err)
	} else if egNews.BasePost.Title != "engadget_news" {
		t.Errorf("embedded struct's value should be scanned correctly")
	}

	var egPosts []EngadgetPost
	if err := DB.Order("author_name asc").Find(&egPosts).Error; err != nil {
		t.Fatalf("no error should happen when query with embedded struct, but got %v", err)
	}
	expectAuthors := []string{"Edward", "George"}
	for i, post := range egPosts {
		t.Log(i, post.Author)
		if want := expectAuthors[i]; post.Author.Name != want {
			t.Errorf("expected author %s got %s", want, post.Author.Name)
		}
	}
}

func TestEmbeddedPointerTypeStruct(t *testing.T) {
	type BasePost struct {
		Id    int64
		Title string
		URL   string
	}

	type Author struct {
		ID          string
		Name        string
		Email       string
		Age         int
		Content     Content
		ContentPtr  *Content
		Birthday    time.Time
		BirthdayPtr *time.Time
	}

	type HNPost struct {
		*BasePost
		Upvotes int32
		*Author `gorm:"EmbeddedPrefix:user_"` // Embedded struct
	}

	DB.Migrator().DropTable(&HNPost{})
	if err := DB.Migrator().AutoMigrate(&HNPost{}); err != nil {
		t.Fatalf("failed to auto migrate, got error: %v", err)
	}

	DB.Create(&HNPost{BasePost: &BasePost{Title: "embedded_pointer_type"}})

	var hnPost HNPost
	if err := DB.First(&hnPost, "title = ?", "embedded_pointer_type").Error; err != nil {
		t.Errorf("No error should happen when find embedded pointer type, but got %v", err)
	}

	if hnPost.Title != "embedded_pointer_type" {
		t.Errorf("Should find correct value for embedded pointer type")
	}

	if hnPost.Author != nil {
		t.Errorf("Expected to get back a nil Author but got: %v", hnPost.Author)
	}

	now := time.Now().Round(time.Second)
	NewPost := HNPost{
		BasePost: &BasePost{Title: "embedded_pointer_type2"},
		Author: &Author{
			Name:        "test",
			Content:     Content{"test"},
			ContentPtr:  nil,
			Birthday:    now,
			BirthdayPtr: nil,
		},
	}
	DB.Create(&NewPost)

	hnPost = HNPost{}
	if err := DB.First(&hnPost, "title = ?", NewPost.Title).Error; err != nil {
		t.Errorf("No error should happen when find embedded pointer type, but got %v", err)
	}

	if hnPost.Title != NewPost.Title {
		t.Errorf("Should find correct value for embedded pointer type")
	}

	if hnPost.Author.Name != NewPost.Author.Name {
		t.Errorf("Expected to get Author name %v but got: %v", NewPost.Author.Name, hnPost.Author.Name)
	}

	if !reflect.DeepEqual(NewPost.Author.Content, hnPost.Author.Content) {
		t.Errorf("Expected to get Author content %v but got: %v", NewPost.Author.Content, hnPost.Author.Content)
	}

	if hnPost.Author.ContentPtr != nil {
		t.Errorf("Expected to get nil Author contentPtr but got: %v", hnPost.Author.ContentPtr)
	}

	if NewPost.Author.Birthday.UnixMilli() != hnPost.Author.Birthday.UnixMilli() {
		t.Errorf("Expected to get Author birthday with %+v but got: %+v", NewPost.Author.Birthday, hnPost.Author.Birthday)
	}

	if hnPost.Author.BirthdayPtr != nil {
		t.Errorf("Expected to get nil Author birthdayPtr but got: %+v", hnPost.Author.BirthdayPtr)
	}
}

type Content struct {
	Content interface{} `gorm:"type:String"`
}

func (c Content) Value() (driver.Value, error) {
	// mssql driver with issue on handling null bytes https://github.com/denisenkom/go-mssqldb/issues/530,
	b, err := json.Marshal(c)
	return string(b[:]), err
}

func (c *Content) Scan(src interface{}) error {
	var value Content
	str, ok := src.(string)
	if !ok {
		byt, ok := src.([]byte)
		if !ok {
			return errors.New("Embedded.Scan byte assertion failed")
		}
		if err := json.Unmarshal(byt, &value); err != nil {
			return err
		}
	} else {
		if err := json.Unmarshal([]byte(str), &value); err != nil {
			return err
		}
	}

	*c = value

	return nil
}

func TestEmbeddedScanValuer(t *testing.T) {
	type HNPost struct {
		gorm.Model
		Content
	}

	DB.Migrator().DropTable(&HNPost{})
	if err := DB.Migrator().AutoMigrate(&HNPost{}); err != nil {
		t.Fatalf("failed to auto migrate, got error: %v", err)
	}

	hnPost := HNPost{Content: Content{Content: "hello world"}}

	if err := DB.Create(&hnPost).Error; err != nil {
		t.Errorf("Failed to create got error %v", err)
	}
}

func TestEmbeddedRelations(t *testing.T) {
	type EmbUser struct {
		gorm.Model
		Name      string
		Age       uint
		Languages []Language `gorm:"many2many:EmbUserSpeak;"`
	}

	type AdvancedUser struct {
		EmbUser  `gorm:"embedded"`
		Advanced bool
	}

	DB.Migrator().DropTable(&AdvancedUser{})

	if err := DB.AutoMigrate(&AdvancedUser{}); err != nil {
		if DB.Dialector.Name() != "sqlite" {
			t.Errorf("Failed to auto migrate advanced user, got error %v", err)
		}
	}
}

func TestEmbeddedTagSetting(t *testing.T) {
	type Tag1 struct {
		Id int64 `gorm:"autoIncrement"`
	}
	type Tag2 struct {
		Id int64
	}

	type EmbeddedTag struct {
		Tag1 Tag1 `gorm:"Embedded;"`
		Tag2 Tag2 `gorm:"Embedded;EmbeddedPrefix:t2_"`
		Name string
	}

	DB.Migrator().DropTable(&EmbeddedTag{})
	err := DB.Migrator().AutoMigrate(&EmbeddedTag{})
	AssertEqual(t, err, nil)

	t1 := EmbeddedTag{Name: "embedded_tag"}
	err = DB.Save(&t1).Error
	AssertEqual(t, err, nil)
	if t1.Tag1.Id == 0 {
		t.Errorf("embedded struct's primary field should be rewritten")
	}
}
