package tests_test

import (
	"reflect"
	"sort"
	"testing"

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

type Blog struct {
	ID         uint   `gorm:"primary_key"`
	Locale     string `gorm:"primary_key"`
	Subject    string
	Body       string
	Tags       []Tag `gorm:"many2many:blog_tags;"`
	SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"`
	LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"`
}

type Tag struct {
	ID     uint   `gorm:"primary_key"`
	Locale string `gorm:"primary_key"`
	Value  string
	Blogs  []*Blog `gorm:"many2many:blog_tags"`
}

func compareTags(tags []Tag, contents []string) bool {
	var tagContents []string
	for _, tag := range tags {
		tagContents = append(tagContents, tag.Value)
	}
	sort.Strings(tagContents)
	sort.Strings(contents)
	return reflect.DeepEqual(tagContents, contents)
}

func TestManyToManyWithMultiPrimaryKeys(t *testing.T) {
	if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" {
		t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment")
	}

	if name := DB.Dialector.Name(); name == "postgres" || name == "mysql" || name == "gaussdb" {
		stmt := gorm.Statement{DB: DB}
		stmt.Parse(&Blog{})
		stmt.Schema.LookUpField("ID").Unique = true
		stmt.Parse(&Tag{})
		stmt.Schema.LookUpField("ID").Unique = true
		// postgers only allow unique constraint matching given keys
	}

	DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags")
	if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil {
		t.Fatalf("Failed to auto migrate, got error: %v", err)
	}

	blog := Blog{
		Locale:  "ZH",
		Subject: "subject",
		Body:    "body",
		Tags: []Tag{
			{Locale: "ZH", Value: "tag1"},
			{Locale: "ZH", Value: "tag2"},
		},
	}

	DB.Save(&blog)
	if !compareTags(blog.Tags, []string{"tag1", "tag2"}) {
		t.Fatalf("Blog should has two tags")
	}

	// Append
	tag3 := &Tag{Locale: "ZH", Value: "tag3"}
	DB.Model(&blog).Association("Tags").Append([]*Tag{tag3})

	if !compareTags(blog.Tags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Blog should has three tags after Append")
	}

	if count := DB.Model(&blog).Association("Tags").Count(); count != 3 {
		t.Fatalf("Blog should has 3 tags after Append, got %v", count)
	}

	var tags []Tag
	DB.Model(&blog).Association("Tags").Find(&tags)
	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Should find 3 tags")
	}

	var blog1 Blog
	DB.Preload("Tags").Find(&blog1)
	if !compareTags(blog1.Tags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Preload many2many relations")
	}

	// Replace
	tag5 := &Tag{Locale: "ZH", Value: "tag5"}
	tag6 := &Tag{Locale: "ZH", Value: "tag6"}
	DB.Model(&blog).Association("Tags").Replace(tag5, tag6)
	var tags2 []Tag
	DB.Model(&blog).Association("Tags").Find(&tags2)
	if !compareTags(tags2, []string{"tag5", "tag6"}) {
		t.Fatalf("Should find 2 tags after Replace")
	}

	if DB.Model(&blog).Association("Tags").Count() != 2 {
		t.Fatalf("Blog should has three tags after Replace")
	}

	// Delete
	DB.Model(&blog).Association("Tags").Delete(tag5)
	var tags3 []Tag
	DB.Model(&blog).Association("Tags").Find(&tags3)
	if !compareTags(tags3, []string{"tag6"}) {
		t.Fatalf("Should find 1 tags after Delete")
	}

	if DB.Model(&blog).Association("Tags").Count() != 1 {
		t.Fatalf("Blog should has three tags after Delete")
	}

	DB.Model(&blog).Association("Tags").Delete(tag3)
	var tags4 []Tag
	DB.Model(&blog).Association("Tags").Find(&tags4)
	if !compareTags(tags4, []string{"tag6"}) {
		t.Fatalf("Tag should not be deleted when Delete with a unrelated tag")
	}

	// Clear
	DB.Model(&blog).Association("Tags").Clear()
	if DB.Model(&blog).Association("Tags").Count() != 0 {
		t.Fatalf("All tags should be cleared")
	}
}

func TestManyToManyWithCustomizedForeignKeys(t *testing.T) {
	if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" {
		t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment")
	}

	if name := DB.Dialector.Name(); name == "postgres" {
		t.Skip("skip postgres due to it only allow unique constraint matching given keys")
	}
	if name := DB.Dialector.Name(); name == "gaussdb" {
		t.Skip("skip gaussdb due to it only allow unique constraint matching given keys")
	}

	DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags")
	if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil {
		t.Fatalf("Failed to auto migrate, got error: %v", err)
	}

	blog := Blog{
		Locale:  "ZH",
		Subject: "subject",
		Body:    "body",
		SharedTags: []Tag{
			{Locale: "ZH", Value: "tag1"},
			{Locale: "ZH", Value: "tag2"},
		},
	}
	DB.Save(&blog)

	blog2 := Blog{
		ID:     blog.ID,
		Locale: "EN",
	}
	DB.Create(&blog2)

	if !compareTags(blog.SharedTags, []string{"tag1", "tag2"}) {
		t.Fatalf("Blog should has two tags")
	}

	// Append
	tag3 := &Tag{Locale: "ZH", Value: "tag3"}
	DB.Model(&blog).Association("SharedTags").Append([]*Tag{tag3})
	if !compareTags(blog.SharedTags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Blog should has three tags after Append")
	}

	if DB.Model(&blog).Association("SharedTags").Count() != 3 {
		t.Fatalf("Blog should has three tags after Append")
	}

	if DB.Model(&blog2).Association("SharedTags").Count() != 3 {
		t.Fatalf("Blog should has three tags after Append")
	}

	var tags []Tag
	DB.Model(&blog).Association("SharedTags").Find(&tags)
	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Should find 3 tags")
	}

	DB.Model(&blog2).Association("SharedTags").Find(&tags)
	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Should find 3 tags")
	}

	var blog1 Blog
	DB.Preload("SharedTags").Find(&blog1)
	if !compareTags(blog1.SharedTags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Preload many2many relations")
	}

	tag4 := &Tag{Locale: "ZH", Value: "tag4"}
	DB.Model(&blog2).Association("SharedTags").Append(tag4)

	DB.Model(&blog).Association("SharedTags").Find(&tags)
	if !compareTags(tags, []string{"tag1", "tag2", "tag3", "tag4"}) {
		t.Fatalf("Should find 3 tags")
	}

	DB.Model(&blog2).Association("SharedTags").Find(&tags)
	if !compareTags(tags, []string{"tag1", "tag2", "tag3", "tag4"}) {
		t.Fatalf("Should find 3 tags")
	}

	// Replace
	tag5 := &Tag{Locale: "ZH", Value: "tag5"}
	tag6 := &Tag{Locale: "ZH", Value: "tag6"}
	DB.Model(&blog2).Association("SharedTags").Replace(tag5, tag6)
	var tags2 []Tag
	DB.Model(&blog).Association("SharedTags").Find(&tags2)
	if !compareTags(tags2, []string{"tag5", "tag6"}) {
		t.Fatalf("Should find 2 tags after Replace")
	}

	DB.Model(&blog2).Association("SharedTags").Find(&tags2)
	if !compareTags(tags2, []string{"tag5", "tag6"}) {
		t.Fatalf("Should find 2 tags after Replace")
	}

	if DB.Model(&blog).Association("SharedTags").Count() != 2 {
		t.Fatalf("Blog should has three tags after Replace")
	}

	// Delete
	DB.Model(&blog).Association("SharedTags").Delete(tag5)
	var tags3 []Tag
	DB.Model(&blog).Association("SharedTags").Find(&tags3)
	if !compareTags(tags3, []string{"tag6"}) {
		t.Fatalf("Should find 1 tags after Delete")
	}

	if DB.Model(&blog).Association("SharedTags").Count() != 1 {
		t.Fatalf("Blog should has three tags after Delete")
	}

	DB.Model(&blog2).Association("SharedTags").Delete(tag3)
	var tags4 []Tag
	DB.Model(&blog).Association("SharedTags").Find(&tags4)
	if !compareTags(tags4, []string{"tag6"}) {
		t.Fatalf("Tag should not be deleted when Delete with a unrelated tag")
	}

	// Clear
	DB.Model(&blog2).Association("SharedTags").Clear()
	if DB.Model(&blog).Association("SharedTags").Count() != 0 {
		t.Fatalf("All tags should be cleared")
	}
}

func TestManyToManyWithCustomizedForeignKeys2(t *testing.T) {
	if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" {
		t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment")
	}

	if name := DB.Dialector.Name(); name == "postgres" || name == "mysql" {
		t.Skip("skip postgres due to it only allow unique constraint matching given keys")
	}

	if name := DB.Dialector.Name(); name == "gaussdb" {
		t.Skip("skip gaussdb due to it only allow unique constraint matching given keys")
	}

	DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags")
	if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil {
		t.Fatalf("Failed to auto migrate, got error: %v", err)
	}

	blog := Blog{
		Locale:  "ZH",
		Subject: "subject",
		Body:    "body",
		LocaleTags: []Tag{
			{Locale: "ZH", Value: "tag1"},
			{Locale: "ZH", Value: "tag2"},
		},
	}
	DB.Save(&blog)

	blog2 := Blog{
		ID:     blog.ID,
		Locale: "EN",
	}
	DB.Create(&blog2)

	// Append
	tag3 := &Tag{Locale: "ZH", Value: "tag3"}
	DB.Model(&blog).Association("LocaleTags").Append([]*Tag{tag3})
	if !compareTags(blog.LocaleTags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Blog should has three tags after Append")
	}

	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
		t.Fatalf("Blog should has three tags after Append")
	}

	if DB.Model(&blog2).Association("LocaleTags").Count() != 0 {
		t.Fatalf("EN Blog should has 0 tags after ZH Blog Append")
	}

	var tags []Tag
	DB.Model(&blog).Association("LocaleTags").Find(&tags)
	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Should find 3 tags")
	}

	DB.Model(&blog2).Association("LocaleTags").Find(&tags)
	if len(tags) != 0 {
		t.Fatalf("Should find 0 tags for EN Blog")
	}

	var blog1 Blog
	DB.Preload("LocaleTags").Find(&blog1, "locale = ? AND id = ?", "ZH", blog.ID)
	if !compareTags(blog1.LocaleTags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Preload many2many relations")
	}

	tag4 := &Tag{Locale: "ZH", Value: "tag4"}
	DB.Model(&blog2).Association("LocaleTags").Append(tag4)

	DB.Model(&blog).Association("LocaleTags").Find(&tags)
	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("Should find 3 tags for EN Blog")
	}

	DB.Model(&blog2).Association("LocaleTags").Find(&tags)
	if !compareTags(tags, []string{"tag4"}) {
		t.Fatalf("Should find 1 tags for EN Blog, but got %v", tags)
	}

	// Replace
	tag5 := &Tag{Locale: "ZH", Value: "tag5"}
	tag6 := &Tag{Locale: "ZH", Value: "tag6"}
	DB.Model(&blog2).Association("LocaleTags").Replace(tag5, tag6)

	var tags2 []Tag
	DB.Model(&blog).Association("LocaleTags").Find(&tags2)
	if !compareTags(tags2, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("CN Blog's tags should not be changed after EN Blog Replace")
	}

	var blog11 Blog
	DB.Preload("LocaleTags").First(&blog11, "id = ? AND locale = ?", blog.ID, blog.Locale)
	if !compareTags(blog11.LocaleTags, []string{"tag1", "tag2", "tag3"}) {
		t.Fatalf("CN Blog's tags should not be changed after EN Blog Replace")
	}

	DB.Model(&blog2).Association("LocaleTags").Find(&tags2)
	if !compareTags(tags2, []string{"tag5", "tag6"}) {
		t.Fatalf("Should find 2 tags after Replace")
	}

	var blog21 Blog
	DB.Preload("LocaleTags").First(&blog21, "id = ? AND locale = ?", blog2.ID, blog2.Locale)
	if !compareTags(blog21.LocaleTags, []string{"tag5", "tag6"}) {
		t.Fatalf("EN Blog's tags should be changed after Replace")
	}

	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
		t.Fatalf("ZH Blog should has three tags after Replace")
	}

	if DB.Model(&blog2).Association("LocaleTags").Count() != 2 {
		t.Fatalf("EN Blog should has two tags after Replace")
	}

	// Delete
	DB.Model(&blog).Association("LocaleTags").Delete(tag5)

	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
		t.Fatalf("ZH Blog should has three tags after Delete with EN's tag")
	}

	if DB.Model(&blog2).Association("LocaleTags").Count() != 2 {
		t.Fatalf("EN Blog should has two tags after ZH Blog Delete with EN's tag")
	}

	DB.Model(&blog2).Association("LocaleTags").Delete(tag5)

	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
		t.Fatalf("ZH Blog should has three tags after EN Blog Delete with EN's tag")
	}

	if DB.Model(&blog2).Association("LocaleTags").Count() != 1 {
		t.Fatalf("EN Blog should has 1 tags after EN Blog Delete with EN's tag")
	}

	// Clear
	DB.Model(&blog2).Association("LocaleTags").Clear()
	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
		t.Fatalf("ZH Blog's tags should not be cleared when clear EN Blog's tags")
	}

	if DB.Model(&blog2).Association("LocaleTags").Count() != 0 {
		t.Fatalf("EN Blog's tags should be cleared when clear EN Blog's tags")
	}

	DB.Model(&blog).Association("LocaleTags").Clear()
	if DB.Model(&blog).Association("LocaleTags").Count() != 0 {
		t.Fatalf("ZH Blog's tags should be cleared when clear ZH Blog's tags")
	}

	if DB.Model(&blog2).Association("LocaleTags").Count() != 0 {
		t.Fatalf("EN Blog's tags should be cleared")
	}
}

func TestCompositePrimaryKeysAssociations(t *testing.T) {
	type Label struct {
		BookID *uint  `gorm:"primarykey"`
		Name   string `gorm:"primarykey"`
		Value  string
	}

	type Book struct {
		ID     int
		Name   string
		Labels []Label
	}

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

	book := Book{
		Name: "my book",
		Labels: []Label{
			{Name: "region", Value: "emea"},
		},
	}

	DB.Create(&book)

	var result Book
	if err := DB.Preload("Labels").First(&result, book.ID).Error; err != nil {
		t.Fatalf("failed to preload, got error %v", err)
	}

	AssertEqual(t, book, result)
}
