package merge

import (
	"strings"
	"testing"

	"git.warky.dev/wdevs/relspecgo/pkg/models"
)

func TestMergeDatabases_NilInputs(t *testing.T) {
	result := MergeDatabases(nil, nil, nil)
	if result == nil {
		t.Fatal("Expected non-nil result")
	}
	if result.SchemasAdded != 0 {
		t.Errorf("Expected 0 schemas added, got %d", result.SchemasAdded)
	}
}

func TestMergeDatabases_NewSchema(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{Name: "public"},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{Name: "auth"},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.SchemasAdded != 1 {
		t.Errorf("Expected 1 schema added, got %d", result.SchemasAdded)
	}
	if len(target.Schemas) != 2 {
		t.Errorf("Expected 2 schemas in target, got %d", len(target.Schemas))
	}
}

func TestMergeDatabases_ExistingSchema(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{Name: "public"},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{Name: "public"},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.SchemasAdded != 0 {
		t.Errorf("Expected 0 schemas added, got %d", result.SchemasAdded)
	}
	if len(target.Schemas) != 1 {
		t.Errorf("Expected 1 schema in target, got %d", len(target.Schemas))
	}
}

func TestMergeTables_NewTable(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "users",
						Schema:  "public",
						Columns: map[string]*models.Column{},
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "posts",
						Schema:  "public",
						Columns: map[string]*models.Column{},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.TablesAdded != 1 {
		t.Errorf("Expected 1 table added, got %d", result.TablesAdded)
	}
	if len(target.Schemas[0].Tables) != 2 {
		t.Errorf("Expected 2 tables in target schema, got %d", len(target.Schemas[0].Tables))
	}
}

func TestMergeColumns_NewColumn(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:   "users",
						Schema: "public",
						Columns: map[string]*models.Column{
							"id": {Name: "id", Type: "int"},
						},
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:   "users",
						Schema: "public",
						Columns: map[string]*models.Column{
							"email": {Name: "email", Type: "varchar"},
						},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.ColumnsAdded != 1 {
		t.Errorf("Expected 1 column added, got %d", result.ColumnsAdded)
	}
	if len(target.Schemas[0].Tables[0].Columns) != 2 {
		t.Errorf("Expected 2 columns in target table, got %d", len(target.Schemas[0].Tables[0].Columns))
	}
}

func TestMergeColumns_TypeConflictIsDetected(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:   "users",
						Schema: "public",
						Columns: map[string]*models.Column{
							"email": {Name: "email", Type: "varchar", Length: 255},
						},
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:   "users",
						Schema: "public",
						Columns: map[string]*models.Column{
							"email": {Name: "email", Type: "text"},
						},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)

	if len(result.TypeConflicts) != 1 {
		t.Fatalf("Expected 1 type conflict, got %d", len(result.TypeConflicts))
	}
	conflict := result.TypeConflicts[0]
	if conflict.Schema != "public" || conflict.Table != "users" || conflict.Column != "email" {
		t.Fatalf("Unexpected conflict location: %+v", conflict)
	}
	if conflict.TargetType != "varchar(255)" {
		t.Fatalf("Expected target type varchar(255), got %q", conflict.TargetType)
	}
	if conflict.SourceType != "text" {
		t.Fatalf("Expected source type text, got %q", conflict.SourceType)
	}

	if got := target.Schemas[0].Tables[0].Columns["email"].Type; got != "varchar" {
		t.Fatalf("Expected target column type to remain unchanged, got %q", got)
	}
}

func TestMergeColumns_SerialVsUnderlyingIntegerIsNotAConflict(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:   "users",
						Schema: "public",
						Columns: map[string]*models.Column{
							// As reported back by a live PostgreSQL read of an
							// existing serial primary key column.
							"id": {Name: "id", Type: "bigint"},
						},
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:   "users",
						Schema: "public",
						Columns: map[string]*models.Column{
							// As declared in a DBML source spec.
							"id": {Name: "id", Type: "bigserial"},
						},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)

	if len(result.TypeConflicts) != 0 {
		t.Fatalf("Expected no type conflicts for bigserial vs bigint, got %d: %+v", len(result.TypeConflicts), result.TypeConflicts)
	}
}

func TestMergeConstraints_NewConstraint(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:        "users",
						Schema:      "public",
						Columns:     map[string]*models.Column{},
						Constraints: map[string]*models.Constraint{},
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "users",
						Schema:  "public",
						Columns: map[string]*models.Column{},
						Constraints: map[string]*models.Constraint{
							"ukey_users_email": {
								Type:    models.UniqueConstraint,
								Columns: []string{"email"},
								Name:    "ukey_users_email",
							},
						},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.ConstraintsAdded != 1 {
		t.Errorf("Expected 1 constraint added, got %d", result.ConstraintsAdded)
	}
	if len(target.Schemas[0].Tables[0].Constraints) != 1 {
		t.Errorf("Expected 1 constraint in target table, got %d", len(target.Schemas[0].Tables[0].Constraints))
	}
}

func TestMergeConstraints_NilConstraintsMap(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:        "users",
						Schema:      "public",
						Columns:     map[string]*models.Column{},
						Constraints: nil, // Nil map
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "users",
						Schema:  "public",
						Columns: map[string]*models.Column{},
						Constraints: map[string]*models.Constraint{
							"ukey_users_email": {
								Type:    models.UniqueConstraint,
								Columns: []string{"email"},
								Name:    "ukey_users_email",
							},
						},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.ConstraintsAdded != 1 {
		t.Errorf("Expected 1 constraint added, got %d", result.ConstraintsAdded)
	}
	if target.Schemas[0].Tables[0].Constraints == nil {
		t.Error("Expected constraints map to be initialized")
	}
	if len(target.Schemas[0].Tables[0].Constraints) != 1 {
		t.Errorf("Expected 1 constraint in target table, got %d", len(target.Schemas[0].Tables[0].Constraints))
	}
}

func TestMergeIndexes_NewIndex(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "users",
						Schema:  "public",
						Columns: map[string]*models.Column{},
						Indexes: map[string]*models.Index{},
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "users",
						Schema:  "public",
						Columns: map[string]*models.Column{},
						Indexes: map[string]*models.Index{
							"idx_users_email": {
								Name:    "idx_users_email",
								Columns: []string{"email"},
							},
						},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.IndexesAdded != 1 {
		t.Errorf("Expected 1 index added, got %d", result.IndexesAdded)
	}
	if len(target.Schemas[0].Tables[0].Indexes) != 1 {
		t.Errorf("Expected 1 index in target table, got %d", len(target.Schemas[0].Tables[0].Indexes))
	}
}

func TestMergeIndexes_NilIndexesMap(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "users",
						Schema:  "public",
						Columns: map[string]*models.Column{},
						Indexes: nil, // Nil map
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "users",
						Schema:  "public",
						Columns: map[string]*models.Column{},
						Indexes: map[string]*models.Index{
							"idx_users_email": {
								Name:    "idx_users_email",
								Columns: []string{"email"},
							},
						},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.IndexesAdded != 1 {
		t.Errorf("Expected 1 index added, got %d", result.IndexesAdded)
	}
	if target.Schemas[0].Tables[0].Indexes == nil {
		t.Error("Expected indexes map to be initialized")
	}
	if len(target.Schemas[0].Tables[0].Indexes) != 1 {
		t.Errorf("Expected 1 index in target table, got %d", len(target.Schemas[0].Tables[0].Indexes))
	}
}

func TestMergeOptions_SkipTableNames(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "users",
						Schema:  "public",
						Columns: map[string]*models.Column{},
					},
				},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:    "migrations",
						Schema:  "public",
						Columns: map[string]*models.Column{},
					},
				},
			},
		},
	}

	opts := &MergeOptions{
		SkipTableNames: map[string]bool{
			"migrations": true,
		},
	}

	result := MergeDatabases(target, source, opts)
	if result.TablesAdded != 0 {
		t.Errorf("Expected 0 tables added (skipped), got %d", result.TablesAdded)
	}
	if len(target.Schemas[0].Tables) != 1 {
		t.Errorf("Expected 1 table in target schema, got %d", len(target.Schemas[0].Tables))
	}
}

func TestMergeViews_NewView(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name:  "public",
				Views: []*models.View{},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Views: []*models.View{
					{
						Name:       "user_summary",
						Schema:     "public",
						Definition: "SELECT * FROM users",
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.ViewsAdded != 1 {
		t.Errorf("Expected 1 view added, got %d", result.ViewsAdded)
	}
	if len(target.Schemas[0].Views) != 1 {
		t.Errorf("Expected 1 view in target schema, got %d", len(target.Schemas[0].Views))
	}
}

func TestMergeEnums_NewEnum(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name:  "public",
				Enums: []*models.Enum{},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Enums: []*models.Enum{
					{
						Name:   "user_role",
						Schema: "public",
						Values: []string{"admin", "user"},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.EnumsAdded != 1 {
		t.Errorf("Expected 1 enum added, got %d", result.EnumsAdded)
	}
	if len(target.Schemas[0].Enums) != 1 {
		t.Errorf("Expected 1 enum in target schema, got %d", len(target.Schemas[0].Enums))
	}
}

func TestMergeDomains_NewDomain(t *testing.T) {
	target := &models.Database{
		Domains: []*models.Domain{},
	}
	source := &models.Database{
		Domains: []*models.Domain{
			{
				Name:        "auth",
				Description: "Authentication domain",
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.DomainsAdded != 1 {
		t.Errorf("Expected 1 domain added, got %d", result.DomainsAdded)
	}
	if len(target.Domains) != 1 {
		t.Errorf("Expected 1 domain in target, got %d", len(target.Domains))
	}
}

func TestMergeRelations_NewRelation(t *testing.T) {
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name:      "public",
				Relations: []*models.Relationship{},
			},
		},
	}
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Relations: []*models.Relationship{
					{
						Name:        "fk_posts_user",
						Type:        models.OneToMany,
						FromTable:   "posts",
						FromColumns: []string{"user_id"},
						ToTable:     "users",
						ToColumns:   []string{"id"},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)
	if result.RelationsAdded != 1 {
		t.Errorf("Expected 1 relation added, got %d", result.RelationsAdded)
	}
	if len(target.Schemas[0].Relations) != 1 {
		t.Errorf("Expected 1 relation in target schema, got %d", len(target.Schemas[0].Relations))
	}
}

func TestGetMergeSummary(t *testing.T) {
	result := &MergeResult{
		SchemasAdded:     1,
		TablesAdded:      2,
		ColumnsAdded:     5,
		ConstraintsAdded: 3,
		IndexesAdded:     2,
		ViewsAdded:       1,
		TypeConflicts: []ColumnTypeConflict{
			{Schema: "public", Table: "users", Column: "email", TargetType: "varchar(255)", SourceType: "text"},
		},
	}

	summary := GetMergeSummary(result)
	if summary == "" {
		t.Error("Expected non-empty summary")
	}
	if len(summary) < 50 {
		t.Errorf("Summary seems too short: %s", summary)
	}
	if !strings.Contains(summary, "Type conflicts:    1") {
		t.Errorf("Expected type conflict count in summary, got: %s", summary)
	}
}

func TestGetMergeSummary_Nil(t *testing.T) {
	summary := GetMergeSummary(nil)
	if summary == "" {
		t.Error("Expected non-empty summary for nil result")
	}
}

func TestComplexMerge(t *testing.T) {
	// Target with existing structure
	target := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:   "users",
						Schema: "public",
						Columns: map[string]*models.Column{
							"id": {Name: "id", Type: "int"},
						},
						Constraints: map[string]*models.Constraint{},
						Indexes:     map[string]*models.Index{},
					},
				},
			},
		},
	}

	// Source with new columns, constraints, and indexes
	source := &models.Database{
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name:   "users",
						Schema: "public",
						Columns: map[string]*models.Column{
							"email": {Name: "email", Type: "varchar"},
							"guid":  {Name: "guid", Type: "uuid"},
						},
						Constraints: map[string]*models.Constraint{
							"ukey_users_email": {
								Type:    models.UniqueConstraint,
								Columns: []string{"email"},
								Name:    "ukey_users_email",
							},
							"ukey_users_guid": {
								Type:    models.UniqueConstraint,
								Columns: []string{"guid"},
								Name:    "ukey_users_guid",
							},
						},
						Indexes: map[string]*models.Index{
							"idx_users_email": {
								Name:    "idx_users_email",
								Columns: []string{"email"},
							},
						},
					},
				},
			},
		},
	}

	result := MergeDatabases(target, source, nil)

	// Verify counts
	if result.ColumnsAdded != 2 {
		t.Errorf("Expected 2 columns added, got %d", result.ColumnsAdded)
	}
	if result.ConstraintsAdded != 2 {
		t.Errorf("Expected 2 constraints added, got %d", result.ConstraintsAdded)
	}
	if result.IndexesAdded != 1 {
		t.Errorf("Expected 1 index added, got %d", result.IndexesAdded)
	}

	// Verify target has merged data
	table := target.Schemas[0].Tables[0]
	if len(table.Columns) != 3 {
		t.Errorf("Expected 3 columns in merged table, got %d", len(table.Columns))
	}
	if len(table.Constraints) != 2 {
		t.Errorf("Expected 2 constraints in merged table, got %d", len(table.Constraints))
	}
	if len(table.Indexes) != 1 {
		t.Errorf("Expected 1 index in merged table, got %d", len(table.Indexes))
	}

	// Verify specific constraint
	if _, exists := table.Constraints["ukey_users_guid"]; !exists {
		t.Error("Expected ukey_users_guid constraint to exist")
	}
}
