package inspector

import (
	"testing"

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

// Helper function to create test database
func createTestDatabase() *models.Database {
	return &models.Database{
		Name: "testdb",
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name: "users",
						Columns: map[string]*models.Column{
							"id": {
								Name:          "id",
								Type:          "bigserial",
								IsPrimaryKey:  true,
								AutoIncrement: true,
							},
							"username": {
								Name:         "username",
								Type:         "varchar(50)",
								NotNull:      true,
								IsPrimaryKey: false,
							},
							"rid_organization": {
								Name:         "rid_organization",
								Type:         "bigint",
								NotNull:      true,
								IsPrimaryKey: false,
							},
						},
						Constraints: map[string]*models.Constraint{
							"fk_users_organization": {
								Name:              "fk_users_organization",
								Type:              models.ForeignKeyConstraint,
								Columns:           []string{"rid_organization"},
								ReferencedTable:   "organizations",
								ReferencedSchema:  "public",
								ReferencedColumns: []string{"id"},
							},
						},
						Indexes: map[string]*models.Index{
							"idx_rid_organization": {
								Name:    "idx_rid_organization",
								Columns: []string{"rid_organization"},
							},
						},
					},
					{
						Name: "organizations",
						Columns: map[string]*models.Column{
							"id": {
								Name:          "id",
								Type:          "bigserial",
								IsPrimaryKey:  true,
								AutoIncrement: true,
							},
							"name": {
								Name:         "name",
								Type:         "varchar(100)",
								NotNull:      true,
								IsPrimaryKey: false,
							},
						},
					},
				},
			},
		},
	}
}

func TestValidatePrimaryKeyNaming(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "matching pattern id",
			rule: Rule{
				Pattern: "^id$",
				Message: "Primary key should be 'id'",
			},
			wantLen:  2,
			wantPass: true,
		},
		{
			name: "non-matching pattern id_",
			rule: Rule{
				Pattern: "^id_",
				Message: "Primary key should start with 'id_'",
			},
			wantLen:  2,
			wantPass: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validatePrimaryKeyNaming(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validatePrimaryKeyNaming() returned %d results, want %d", len(results), tt.wantLen)
			}
			if len(results) > 0 && results[0].Passed != tt.wantPass {
				t.Errorf("validatePrimaryKeyNaming() passed=%v, want %v", results[0].Passed, tt.wantPass)
			}
		})
	}
}

func TestValidatePrimaryKeyDatatype(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "allowed type bigserial",
			rule: Rule{
				AllowedTypes: []string{"bigserial", "bigint", "int"},
				Message:      "Primary key should use integer types",
			},
			wantLen:  2,
			wantPass: true,
		},
		{
			name: "disallowed type",
			rule: Rule{
				AllowedTypes: []string{"uuid"},
				Message:      "Primary key should use UUID",
			},
			wantLen:  2,
			wantPass: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validatePrimaryKeyDatatype(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validatePrimaryKeyDatatype() returned %d results, want %d", len(results), tt.wantLen)
			}
			if len(results) > 0 && results[0].Passed != tt.wantPass {
				t.Errorf("validatePrimaryKeyDatatype() passed=%v, want %v", results[0].Passed, tt.wantPass)
			}
		})
	}
}

func TestValidatePrimaryKeyAutoIncrement(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name    string
		rule    Rule
		wantLen int
	}{
		{
			name: "require auto increment",
			rule: Rule{
				RequireAutoIncrement: true,
				Message:              "Primary key should have auto-increment",
			},
			wantLen: 0, // No violations - all PKs have auto-increment
		},
		{
			name: "disallow auto increment",
			rule: Rule{
				RequireAutoIncrement: false,
				Message:              "Primary key should not have auto-increment",
			},
			wantLen: 2, // 2 violations - both PKs have auto-increment
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validatePrimaryKeyAutoIncrement(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validatePrimaryKeyAutoIncrement() returned %d results, want %d", len(results), tt.wantLen)
			}
		})
	}
}

func TestValidateForeignKeyColumnNaming(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "matching pattern rid_",
			rule: Rule{
				Pattern: "^rid_",
				Message: "Foreign key columns should start with 'rid_'",
			},
			wantLen:  1,
			wantPass: true,
		},
		{
			name: "non-matching pattern fk_",
			rule: Rule{
				Pattern: "^fk_",
				Message: "Foreign key columns should start with 'fk_'",
			},
			wantLen:  1,
			wantPass: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validateForeignKeyColumnNaming(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validateForeignKeyColumnNaming() returned %d results, want %d", len(results), tt.wantLen)
			}
			if len(results) > 0 && results[0].Passed != tt.wantPass {
				t.Errorf("validateForeignKeyColumnNaming() passed=%v, want %v", results[0].Passed, tt.wantPass)
			}
		})
	}
}

func TestValidateForeignKeyConstraintNaming(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "matching pattern fk_",
			rule: Rule{
				Pattern: "^fk_",
				Message: "Foreign key constraints should start with 'fk_'",
			},
			wantLen:  1,
			wantPass: true,
		},
		{
			name: "non-matching pattern FK_",
			rule: Rule{
				Pattern: "^FK_",
				Message: "Foreign key constraints should start with 'FK_'",
			},
			wantLen:  1,
			wantPass: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validateForeignKeyConstraintNaming(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validateForeignKeyConstraintNaming() returned %d results, want %d", len(results), tt.wantLen)
			}
			if len(results) > 0 && results[0].Passed != tt.wantPass {
				t.Errorf("validateForeignKeyConstraintNaming() passed=%v, want %v", results[0].Passed, tt.wantPass)
			}
		})
	}
}

func TestValidateForeignKeyIndex(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "require index with index present",
			rule: Rule{
				RequireIndex: true,
				Message:      "Foreign key columns should have indexes",
			},
			wantLen:  1,
			wantPass: true,
		},
		{
			name: "no requirement",
			rule: Rule{
				RequireIndex: false,
				Message:      "Foreign key index check disabled",
			},
			wantLen:  0,
			wantPass: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validateForeignKeyIndex(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validateForeignKeyIndex() returned %d results, want %d", len(results), tt.wantLen)
			}
			if len(results) > 0 && results[0].Passed != tt.wantPass {
				t.Errorf("validateForeignKeyIndex() passed=%v, want %v", results[0].Passed, tt.wantPass)
			}
		})
	}
}

func TestValidateTableNamingCase(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "lowercase snake_case pattern",
			rule: Rule{
				Pattern: "^[a-z][a-z0-9_]*$",
				Case:    "lowercase",
				Message: "Table names should be lowercase snake_case",
			},
			wantLen:  2,
			wantPass: true,
		},
		{
			name: "uppercase pattern",
			rule: Rule{
				Pattern: "^[A-Z][A-Z0-9_]*$",
				Case:    "uppercase",
				Message: "Table names should be uppercase",
			},
			wantLen:  2,
			wantPass: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validateTableNamingCase(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validateTableNamingCase() returned %d results, want %d", len(results), tt.wantLen)
			}
			if len(results) > 0 && results[0].Passed != tt.wantPass {
				t.Errorf("validateTableNamingCase() passed=%v, want %v", results[0].Passed, tt.wantPass)
			}
		})
	}
}

func TestValidateColumnNamingCase(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "lowercase snake_case pattern",
			rule: Rule{
				Pattern: "^[a-z][a-z0-9_]*$",
				Case:    "lowercase",
				Message: "Column names should be lowercase snake_case",
			},
			wantLen:  5, // 5 total columns across both tables
			wantPass: true,
		},
		{
			name: "camelCase pattern",
			rule: Rule{
				Pattern: "^[a-z][a-zA-Z0-9]*$",
				Case:    "camelCase",
				Message: "Column names should be camelCase",
			},
			wantLen:  5,
			wantPass: false, // rid_organization has underscore
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validateColumnNamingCase(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validateColumnNamingCase() returned %d results, want %d", len(results), tt.wantLen)
			}
		})
	}
}

func TestValidateTableNameLength(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "max length 64",
			rule: Rule{
				MaxLength: 64,
				Message:   "Table name too long",
			},
			wantLen:  2,
			wantPass: true,
		},
		{
			name: "max length 5",
			rule: Rule{
				MaxLength: 5,
				Message:   "Table name too long",
			},
			wantLen:  2,
			wantPass: false, // "users" is 5 chars (passes), "organizations" is 13 (fails)
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validateTableNameLength(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validateTableNameLength() returned %d results, want %d", len(results), tt.wantLen)
			}
		})
	}
}

func TestValidateColumnNameLength(t *testing.T) {
	db := createTestDatabase()

	tests := []struct {
		name     string
		rule     Rule
		wantLen  int
		wantPass bool
	}{
		{
			name: "max length 64",
			rule: Rule{
				MaxLength: 64,
				Message:   "Column name too long",
			},
			wantLen:  5,
			wantPass: true,
		},
		{
			name: "max length 5",
			rule: Rule{
				MaxLength: 5,
				Message:   "Column name too long",
			},
			wantLen:  5,
			wantPass: false, // Some columns exceed 5 chars
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validateColumnNameLength(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validateColumnNameLength() returned %d results, want %d", len(results), tt.wantLen)
			}
		})
	}
}

func TestValidateReservedKeywords(t *testing.T) {
	// Create a database with reserved keywords
	db := &models.Database{
		Name: "testdb",
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name: "user", // "user" is a reserved keyword
						Columns: map[string]*models.Column{
							"id": {
								Name:         "id",
								Type:         "bigint",
								IsPrimaryKey: true,
							},
							"select": { // "select" is a reserved keyword
								Name: "select",
								Type: "varchar(50)",
							},
						},
					},
				},
			},
		},
	}

	tests := []struct {
		name        string
		rule        Rule
		wantLen     int
		checkPasses bool
	}{
		{
			name: "check tables only",
			rule: Rule{
				CheckTables:  true,
				CheckColumns: false,
				Message:      "Reserved keyword used",
			},
			wantLen:     1, // "user" table
			checkPasses: false,
		},
		{
			name: "check columns only",
			rule: Rule{
				CheckTables:  false,
				CheckColumns: true,
				Message:      "Reserved keyword used",
			},
			wantLen:     2, // "id", "select" columns (id passes, select fails)
			checkPasses: false,
		},
		{
			name: "check both",
			rule: Rule{
				CheckTables:  true,
				CheckColumns: true,
				Message:      "Reserved keyword used",
			},
			wantLen:     3, // "user" table + "id", "select" columns
			checkPasses: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			results := validateReservedKeywords(db, tt.rule, "test_rule")
			if len(results) != tt.wantLen {
				t.Errorf("validateReservedKeywords() returned %d results, want %d", len(results), tt.wantLen)
			}
		})
	}
}

func TestValidateMissingPrimaryKey(t *testing.T) {
	// Create database with and without primary keys
	db := &models.Database{
		Name: "testdb",
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name: "with_pk",
						Columns: map[string]*models.Column{
							"id": {
								Name:         "id",
								Type:         "bigint",
								IsPrimaryKey: true,
							},
						},
					},
					{
						Name: "without_pk",
						Columns: map[string]*models.Column{
							"name": {
								Name: "name",
								Type: "varchar(50)",
							},
						},
					},
				},
			},
		},
	}

	rule := Rule{
		Message: "Table missing primary key",
	}

	results := validateMissingPrimaryKey(db, rule, "test_rule")

	if len(results) != 2 {
		t.Errorf("validateMissingPrimaryKey() returned %d results, want 2", len(results))
	}

	// First result should pass (with_pk has PK)
	if results[0].Passed != true {
		t.Errorf("validateMissingPrimaryKey() result[0].Passed=%v, want true", results[0].Passed)
	}

	// Second result should fail (without_pk missing PK)
	if results[1].Passed != false {
		t.Errorf("validateMissingPrimaryKey() result[1].Passed=%v, want false", results[1].Passed)
	}
}

func TestValidateOrphanedForeignKey(t *testing.T) {
	// Create database with orphaned FK
	db := &models.Database{
		Name: "testdb",
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name: "users",
						Columns: map[string]*models.Column{
							"id": {
								Name:         "id",
								Type:         "bigint",
								IsPrimaryKey: true,
							},
						},
						Constraints: map[string]*models.Constraint{
							"fk_nonexistent": {
								Name:             "fk_nonexistent",
								Type:             models.ForeignKeyConstraint,
								Columns:          []string{"rid_organization"},
								ReferencedTable:  "nonexistent_table",
								ReferencedSchema: "public",
							},
						},
					},
				},
			},
		},
	}

	rule := Rule{
		Message: "Foreign key references non-existent table",
	}

	results := validateOrphanedForeignKey(db, rule, "test_rule")

	if len(results) != 1 {
		t.Errorf("validateOrphanedForeignKey() returned %d results, want 1", len(results))
	}

	if results[0].Passed != false {
		t.Errorf("validateOrphanedForeignKey() passed=%v, want false", results[0].Passed)
	}
}

func TestValidateCircularDependency(t *testing.T) {
	// Create database with circular dependency
	db := &models.Database{
		Name: "testdb",
		Schemas: []*models.Schema{
			{
				Name: "public",
				Tables: []*models.Table{
					{
						Name: "table_a",
						Columns: map[string]*models.Column{
							"id": {Name: "id", Type: "bigint", IsPrimaryKey: true},
						},
						Constraints: map[string]*models.Constraint{
							"fk_to_b": {
								Name:             "fk_to_b",
								Type:             models.ForeignKeyConstraint,
								ReferencedTable:  "table_b",
								ReferencedSchema: "public",
							},
						},
					},
					{
						Name: "table_b",
						Columns: map[string]*models.Column{
							"id": {Name: "id", Type: "bigint", IsPrimaryKey: true},
						},
						Constraints: map[string]*models.Constraint{
							"fk_to_a": {
								Name:             "fk_to_a",
								Type:             models.ForeignKeyConstraint,
								ReferencedTable:  "table_a",
								ReferencedSchema: "public",
							},
						},
					},
				},
			},
		},
	}

	rule := Rule{
		Message: "Circular dependency detected",
	}

	results := validateCircularDependency(db, rule, "test_rule")

	// Should detect circular dependency in both tables
	if len(results) == 0 {
		t.Error("validateCircularDependency() returned 0 results, expected circular dependency detection")
	}

	for _, result := range results {
		if result.Passed {
			t.Error("validateCircularDependency() passed=true, want false for circular dependency")
		}
	}
}

func TestNormalizeDataType(t *testing.T) {
	tests := []struct {
		input    string
		expected string
	}{
		{"varchar(50)", "varchar"},
		{"decimal(10,2)", "decimal"},
		{"int", "int"},
		{"BIGINT", "bigint"},
		{"VARCHAR(255)", "varchar"},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			result := normalizeDataType(tt.input)
			if result != tt.expected {
				t.Errorf("normalizeDataType(%q) = %q, want %q", tt.input, result, tt.expected)
			}
		})
	}
}

func TestContains(t *testing.T) {
	tests := []struct {
		name     string
		slice    []string
		value    string
		expected bool
	}{
		{"found exact", []string{"foo", "bar", "baz"}, "bar", true},
		{"not found", []string{"foo", "bar", "baz"}, "qux", false},
		{"case insensitive match", []string{"foo", "Bar", "baz"}, "bar", true},
		{"empty slice", []string{}, "foo", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := contains(tt.slice, tt.value)
			if result != tt.expected {
				t.Errorf("contains(%v, %q) = %v, want %v", tt.slice, tt.value, result, tt.expected)
			}
		})
	}
}

func TestHasCycle(t *testing.T) {
	tests := []struct {
		name     string
		graph    map[string][]string
		node     string
		expected bool
	}{
		{
			name: "simple cycle",
			graph: map[string][]string{
				"A": {"B"},
				"B": {"C"},
				"C": {"A"},
			},
			node:     "A",
			expected: true,
		},
		{
			name: "no cycle",
			graph: map[string][]string{
				"A": {"B"},
				"B": {"C"},
				"C": {},
			},
			node:     "A",
			expected: false,
		},
		{
			name: "self cycle",
			graph: map[string][]string{
				"A": {"A"},
			},
			node:     "A",
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			visited := make(map[string]bool)
			recStack := make(map[string]bool)
			result := hasCycle(tt.node, tt.graph, visited, recStack)
			if result != tt.expected {
				t.Errorf("hasCycle() = %v, want %v", result, tt.expected)
			}
		})
	}
}

func TestFormatLocation(t *testing.T) {
	tests := []struct {
		schema   string
		table    string
		column   string
		expected string
	}{
		{"public", "users", "id", "public.users.id"},
		{"public", "users", "", "public.users"},
		{"public", "", "", "public"},
	}

	for _, tt := range tests {
		t.Run(tt.expected, func(t *testing.T) {
			result := formatLocation(tt.schema, tt.table, tt.column)
			if result != tt.expected {
				t.Errorf("formatLocation(%q, %q, %q) = %q, want %q",
					tt.schema, tt.table, tt.column, result, tt.expected)
			}
		})
	}
}
