package commontypes

import (
	"testing"
)

func TestExtractBaseType(t *testing.T) {
	tests := []struct {
		name    string
		sqlType string
		want    string
	}{
		{"varchar with length", "varchar(100)", "varchar"},
		{"VARCHAR uppercase with length", "VARCHAR(255)", "varchar"},
		{"numeric with precision", "numeric(10,2)", "numeric"},
		{"NUMERIC uppercase", "NUMERIC(18,4)", "numeric"},
		{"decimal with precision", "decimal(15,3)", "decimal"},
		{"char with length", "char(50)", "char"},
		{"simple integer", "integer", "integer"},
		{"simple text", "text", "text"},
		{"bigint", "bigint", "bigint"},
		{"With spaces", " varchar(100) ", "varchar"},
		{"No parentheses", "boolean", "boolean"},
		{"Empty string", "", ""},
		{"Mixed case", "VarChar(100)", "varchar"},
		{"timestamp with time zone", "timestamp(6) with time zone", "timestamp"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := ExtractBaseType(tt.sqlType)
			if got != tt.want {
				t.Errorf("ExtractBaseType(%q) = %q, want %q", tt.sqlType, got, tt.want)
			}
		})
	}
}

func TestNormalizeType(t *testing.T) {
	// NormalizeType is an alias for ExtractBaseType, test that they behave the same
	testCases := []string{
		"varchar(100)",
		"numeric(10,2)",
		"integer",
		"text",
		" VARCHAR(255) ",
	}

	for _, tc := range testCases {
		t.Run(tc, func(t *testing.T) {
			extracted := ExtractBaseType(tc)
			normalized := NormalizeType(tc)
			if extracted != normalized {
				t.Errorf("ExtractBaseType(%q) = %q, but NormalizeType(%q) = %q",
					tc, extracted, tc, normalized)
			}
		})
	}
}

func TestSQLToGo(t *testing.T) {
	tests := []struct {
		name     string
		sqlType  string
		nullable bool
		want     string
	}{
		// Integer types (nullable)
		{"integer nullable", "integer", true, "int32"},
		{"bigint nullable", "bigint", true, "int64"},
		{"smallint nullable", "smallint", true, "int16"},
		{"serial nullable", "serial", true, "int32"},

		// Integer types (not nullable)
		{"integer not nullable", "integer", false, "*int32"},
		{"bigint not nullable", "bigint", false, "*int64"},
		{"smallint not nullable", "smallint", false, "*int16"},

		// String types (nullable)
		{"text nullable", "text", true, "string"},
		{"varchar nullable", "varchar", true, "string"},
		{"varchar with length nullable", "varchar(100)", true, "string"},

		// String types (not nullable)
		{"text not nullable", "text", false, "*string"},
		{"varchar not nullable", "varchar", false, "*string"},

		// Boolean
		{"boolean nullable", "boolean", true, "bool"},
		{"boolean not nullable", "boolean", false, "*bool"},

		// Float types
		{"real nullable", "real", true, "float32"},
		{"double precision nullable", "double precision", true, "float64"},
		{"real not nullable", "real", false, "*float32"},
		{"double precision not nullable", "double precision", false, "*float64"},

		// Date/Time types
		{"timestamp nullable", "timestamp", true, "time.Time"},
		{"date nullable", "date", true, "time.Time"},
		{"timestamp not nullable", "timestamp", false, "*time.Time"},

		// Binary
		{"bytea nullable", "bytea", true, "[]byte"},
		{"bytea not nullable", "bytea", false, "[]byte"}, // Slices don't get pointer

		// UUID
		{"uuid nullable", "uuid", true, "string"},
		{"uuid not nullable", "uuid", false, "*string"},

		// JSON
		{"json nullable", "json", true, "string"},
		{"jsonb nullable", "jsonb", true, "string"},

		// Array
		{"array nullable", "array", true, "[]string"},
		{"array not nullable", "array", false, "[]string"}, // Slices don't get pointer

		// Unknown types
		{"unknown type nullable", "unknowntype", true, "interface{}"},
		{"unknown type not nullable", "unknowntype", false, "interface{}"}, // Interface doesn't get pointer
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := SQLToGo(tt.sqlType, tt.nullable)
			if got != tt.want {
				t.Errorf("SQLToGo(%q, %v) = %q, want %q", tt.sqlType, tt.nullable, got, tt.want)
			}
		})
	}
}

func TestSQLToTypeScript(t *testing.T) {
	tests := []struct {
		name     string
		sqlType  string
		nullable bool
		want     string
	}{
		// Integer types
		{"integer nullable", "integer", true, "number"},
		{"integer not nullable", "integer", false, "number | null"},
		{"bigint nullable", "bigint", true, "number"},
		{"bigint not nullable", "bigint", false, "number | null"},

		// String types
		{"text nullable", "text", true, "string"},
		{"text not nullable", "text", false, "string | null"},
		{"varchar nullable", "varchar", true, "string"},
		{"varchar(100) nullable", "varchar(100)", true, "string"},

		// Boolean
		{"boolean nullable", "boolean", true, "boolean"},
		{"boolean not nullable", "boolean", false, "boolean | null"},

		// Float types
		{"real nullable", "real", true, "number"},
		{"double precision nullable", "double precision", true, "number"},

		// Date/Time types
		{"timestamp nullable", "timestamp", true, "Date"},
		{"date nullable", "date", true, "Date"},
		{"timestamp not nullable", "timestamp", false, "Date | null"},

		// Binary
		{"bytea nullable", "bytea", true, "Buffer"},
		{"bytea not nullable", "bytea", false, "Buffer | null"},

		// JSON
		{"json nullable", "json", true, "any"},
		{"jsonb nullable", "jsonb", true, "any"},

		// UUID
		{"uuid nullable", "uuid", true, "string"},

		// Unknown types
		{"unknown type nullable", "unknowntype", true, "any"},
		{"unknown type not nullable", "unknowntype", false, "any | null"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := SQLToTypeScript(tt.sqlType, tt.nullable)
			if got != tt.want {
				t.Errorf("SQLToTypeScript(%q, %v) = %q, want %q", tt.sqlType, tt.nullable, got, tt.want)
			}
		})
	}
}

func TestSQLToPython(t *testing.T) {
	tests := []struct {
		name    string
		sqlType string
		want    string
	}{
		// Integer types
		{"integer", "integer", "int"},
		{"bigint", "bigint", "int"},
		{"smallint", "smallint", "int"},

		// String types
		{"text", "text", "str"},
		{"varchar", "varchar", "str"},
		{"varchar(100)", "varchar(100)", "str"},

		// Boolean
		{"boolean", "boolean", "bool"},

		// Float types
		{"real", "real", "float"},
		{"double precision", "double precision", "float"},
		{"numeric", "numeric", "Decimal"},
		{"decimal", "decimal", "Decimal"},

		// Date/Time types
		{"timestamp", "timestamp", "datetime"},
		{"date", "date", "date"},
		{"time", "time", "time"},

		// Binary
		{"bytea", "bytea", "bytes"},

		// JSON
		{"json", "json", "dict"},
		{"jsonb", "jsonb", "dict"},

		// UUID
		{"uuid", "uuid", "UUID"},

		// Array
		{"array", "array", "list"},

		// Unknown types
		{"unknown type", "unknowntype", "Any"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := SQLToPython(tt.sqlType)
			if got != tt.want {
				t.Errorf("SQLToPython(%q) = %q, want %q", tt.sqlType, got, tt.want)
			}
		})
	}
}

func TestSQLToCSharp(t *testing.T) {
	tests := []struct {
		name     string
		sqlType  string
		nullable bool
		want     string
	}{
		// Integer types (nullable)
		{"integer nullable", "integer", true, "int"},
		{"bigint nullable", "bigint", true, "long"},
		{"smallint nullable", "smallint", true, "short"},

		// Integer types (not nullable - value types get ?)
		{"integer not nullable", "integer", false, "int?"},
		{"bigint not nullable", "bigint", false, "long?"},
		{"smallint not nullable", "smallint", false, "short?"},

		// String types (reference types, no ? needed)
		{"text nullable", "text", true, "string"},
		{"text not nullable", "text", false, "string"},
		{"varchar nullable", "varchar", true, "string"},
		{"varchar(100) nullable", "varchar(100)", true, "string"},

		// Boolean
		{"boolean nullable", "boolean", true, "bool"},
		{"boolean not nullable", "boolean", false, "bool?"},

		// Float types
		{"real nullable", "real", true, "float"},
		{"double precision nullable", "double precision", true, "double"},
		{"decimal nullable", "decimal", true, "decimal"},
		{"real not nullable", "real", false, "float?"},
		{"double precision not nullable", "double precision", false, "double?"},
		{"decimal not nullable", "decimal", false, "decimal?"},

		// Date/Time types
		{"timestamp nullable", "timestamp", true, "DateTime"},
		{"date nullable", "date", true, "DateTime"},
		{"timestamptz nullable", "timestamptz", true, "DateTimeOffset"},
		{"timestamp not nullable", "timestamp", false, "DateTime?"},
		{"timestamptz not nullable", "timestamptz", false, "DateTimeOffset?"},

		// Binary (array type, no ?)
		{"bytea nullable", "bytea", true, "byte[]"},
		{"bytea not nullable", "bytea", false, "byte[]"},

		// UUID
		{"uuid nullable", "uuid", true, "Guid"},
		{"uuid not nullable", "uuid", false, "Guid?"},

		// JSON
		{"json nullable", "json", true, "string"},

		// Unknown types (object is reference type)
		{"unknown type nullable", "unknowntype", true, "object"},
		{"unknown type not nullable", "unknowntype", false, "object"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := SQLToCSharp(tt.sqlType, tt.nullable)
			if got != tt.want {
				t.Errorf("SQLToCSharp(%q, %v) = %q, want %q", tt.sqlType, tt.nullable, got, tt.want)
			}
		})
	}
}

func TestNeedsTimeImport(t *testing.T) {
	tests := []struct {
		name   string
		goType string
		want   bool
	}{
		{"time.Time type", "time.Time", true},
		{"pointer to time.Time", "*time.Time", true},
		{"int32 type", "int32", false},
		{"string type", "string", false},
		{"bool type", "bool", false},
		{"[]byte type", "[]byte", false},
		{"interface{}", "interface{}", false},
		{"empty string", "", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NeedsTimeImport(tt.goType)
			if got != tt.want {
				t.Errorf("NeedsTimeImport(%q) = %v, want %v", tt.goType, got, tt.want)
			}
		})
	}
}

func TestGoTypeMap(t *testing.T) {
	// Test that the map contains expected entries
	expectedMappings := map[string]string{
		"integer":          "int32",
		"bigint":           "int64",
		"text":             "string",
		"boolean":          "bool",
		"double precision": "float64",
		"bytea":            "[]byte",
		"timestamp":        "time.Time",
		"uuid":             "string",
		"json":             "string",
	}

	for sqlType, expectedGoType := range expectedMappings {
		if goType, ok := GoTypeMap[sqlType]; !ok {
			t.Errorf("GoTypeMap missing entry for %q", sqlType)
		} else if goType != expectedGoType {
			t.Errorf("GoTypeMap[%q] = %q, want %q", sqlType, goType, expectedGoType)
		}
	}

	if len(GoTypeMap) == 0 {
		t.Error("GoTypeMap is empty")
	}
}

func TestTypeScriptTypeMap(t *testing.T) {
	expectedMappings := map[string]string{
		"integer":          "number",
		"bigint":           "number",
		"text":             "string",
		"boolean":          "boolean",
		"double precision": "number",
		"bytea":            "Buffer",
		"timestamp":        "Date",
		"uuid":             "string",
		"json":             "any",
	}

	for sqlType, expectedTSType := range expectedMappings {
		if tsType, ok := TypeScriptTypeMap[sqlType]; !ok {
			t.Errorf("TypeScriptTypeMap missing entry for %q", sqlType)
		} else if tsType != expectedTSType {
			t.Errorf("TypeScriptTypeMap[%q] = %q, want %q", sqlType, tsType, expectedTSType)
		}
	}

	if len(TypeScriptTypeMap) == 0 {
		t.Error("TypeScriptTypeMap is empty")
	}
}

func TestPythonTypeMap(t *testing.T) {
	expectedMappings := map[string]string{
		"integer": "int",
		"bigint":  "int",
		"text":    "str",
		"boolean": "bool",
		"real":    "float",
		"numeric": "Decimal",
		"bytea":   "bytes",
		"date":    "date",
		"uuid":    "UUID",
		"json":    "dict",
	}

	for sqlType, expectedPyType := range expectedMappings {
		if pyType, ok := PythonTypeMap[sqlType]; !ok {
			t.Errorf("PythonTypeMap missing entry for %q", sqlType)
		} else if pyType != expectedPyType {
			t.Errorf("PythonTypeMap[%q] = %q, want %q", sqlType, pyType, expectedPyType)
		}
	}

	if len(PythonTypeMap) == 0 {
		t.Error("PythonTypeMap is empty")
	}
}

func TestCSharpTypeMap(t *testing.T) {
	expectedMappings := map[string]string{
		"integer":          "int",
		"bigint":           "long",
		"smallint":         "short",
		"text":             "string",
		"boolean":          "bool",
		"double precision": "double",
		"decimal":          "decimal",
		"bytea":            "byte[]",
		"timestamp":        "DateTime",
		"uuid":             "Guid",
		"json":             "string",
	}

	for sqlType, expectedCSType := range expectedMappings {
		if csType, ok := CSharpTypeMap[sqlType]; !ok {
			t.Errorf("CSharpTypeMap missing entry for %q", sqlType)
		} else if csType != expectedCSType {
			t.Errorf("CSharpTypeMap[%q] = %q, want %q", sqlType, csType, expectedCSType)
		}
	}

	if len(CSharpTypeMap) == 0 {
		t.Error("CSharpTypeMap is empty")
	}
}

func TestSQLToJava(t *testing.T) {
	tests := []struct {
		name     string
		sqlType  string
		nullable bool
		want     string
	}{
		// Integer types
		{"integer nullable", "integer", true, "Integer"},
		{"integer not nullable", "integer", false, "Integer"},
		{"bigint nullable", "bigint", true, "Long"},
		{"smallint nullable", "smallint", true, "Short"},

		// String types
		{"text nullable", "text", true, "String"},
		{"varchar nullable", "varchar", true, "String"},
		{"varchar(100) nullable", "varchar(100)", true, "String"},

		// Boolean
		{"boolean nullable", "boolean", true, "Boolean"},

		// Float types
		{"real nullable", "real", true, "Float"},
		{"double precision nullable", "double precision", true, "Double"},
		{"numeric nullable", "numeric", true, "BigDecimal"},

		// Date/Time types
		{"timestamp nullable", "timestamp", true, "Timestamp"},
		{"date nullable", "date", true, "Date"},
		{"time nullable", "time", true, "Time"},

		// Binary
		{"bytea nullable", "bytea", true, "byte[]"},

		// UUID
		{"uuid nullable", "uuid", true, "UUID"},

		// JSON
		{"json nullable", "json", true, "String"},

		// Unknown types
		{"unknown type nullable", "unknowntype", true, "Object"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := SQLToJava(tt.sqlType, tt.nullable)
			if got != tt.want {
				t.Errorf("SQLToJava(%q, %v) = %q, want %q", tt.sqlType, tt.nullable, got, tt.want)
			}
		})
	}
}

func TestSQLToPhp(t *testing.T) {
	tests := []struct {
		name     string
		sqlType  string
		nullable bool
		want     string
	}{
		// Integer types (nullable)
		{"integer nullable", "integer", true, "int"},
		{"bigint nullable", "bigint", true, "int"},
		{"smallint nullable", "smallint", true, "int"},

		// Integer types (not nullable)
		{"integer not nullable", "integer", false, "?int"},
		{"bigint not nullable", "bigint", false, "?int"},

		// String types
		{"text nullable", "text", true, "string"},
		{"text not nullable", "text", false, "?string"},
		{"varchar nullable", "varchar", true, "string"},
		{"varchar(100) nullable", "varchar(100)", true, "string"},

		// Boolean
		{"boolean nullable", "boolean", true, "bool"},
		{"boolean not nullable", "boolean", false, "?bool"},

		// Float types
		{"real nullable", "real", true, "float"},
		{"double precision nullable", "double precision", true, "float"},
		{"real not nullable", "real", false, "?float"},

		// Date/Time types
		{"timestamp nullable", "timestamp", true, "\\DateTime"},
		{"date nullable", "date", true, "\\DateTime"},
		{"timestamp not nullable", "timestamp", false, "?\\DateTime"},

		// Binary
		{"bytea nullable", "bytea", true, "string"},
		{"bytea not nullable", "bytea", false, "?string"},

		// JSON
		{"json nullable", "json", true, "array"},
		{"json not nullable", "json", false, "?array"},

		// UUID
		{"uuid nullable", "uuid", true, "string"},

		// Unknown types
		{"unknown type nullable", "unknowntype", true, "mixed"},
		{"unknown type not nullable", "unknowntype", false, "mixed"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := SQLToPhp(tt.sqlType, tt.nullable)
			if got != tt.want {
				t.Errorf("SQLToPhp(%q, %v) = %q, want %q", tt.sqlType, tt.nullable, got, tt.want)
			}
		})
	}
}

func TestSQLToRust(t *testing.T) {
	tests := []struct {
		name     string
		sqlType  string
		nullable bool
		want     string
	}{
		// Integer types (nullable)
		{"integer nullable", "integer", true, "i32"},
		{"bigint nullable", "bigint", true, "i64"},
		{"smallint nullable", "smallint", true, "i16"},

		// Integer types (not nullable)
		{"integer not nullable", "integer", false, "Option<i32>"},
		{"bigint not nullable", "bigint", false, "Option<i64>"},
		{"smallint not nullable", "smallint", false, "Option<i16>"},

		// String types
		{"text nullable", "text", true, "String"},
		{"text not nullable", "text", false, "Option<String>"},
		{"varchar nullable", "varchar", true, "String"},
		{"varchar(100) nullable", "varchar(100)", true, "String"},

		// Boolean
		{"boolean nullable", "boolean", true, "bool"},
		{"boolean not nullable", "boolean", false, "Option<bool>"},

		// Float types
		{"real nullable", "real", true, "f32"},
		{"double precision nullable", "double precision", true, "f64"},
		{"real not nullable", "real", false, "Option<f32>"},
		{"double precision not nullable", "double precision", false, "Option<f64>"},

		// Date/Time types
		{"timestamp nullable", "timestamp", true, "NaiveDateTime"},
		{"timestamptz nullable", "timestamptz", true, "DateTime<Utc>"},
		{"date nullable", "date", true, "NaiveDate"},
		{"time nullable", "time", true, "NaiveTime"},
		{"timestamp not nullable", "timestamp", false, "Option<NaiveDateTime>"},

		// Binary
		{"bytea nullable", "bytea", true, "Vec<u8>"},
		{"bytea not nullable", "bytea", false, "Option<Vec<u8>>"},

		// JSON
		{"json nullable", "json", true, "serde_json::Value"},
		{"json not nullable", "json", false, "Option<serde_json::Value>"},

		// UUID
		{"uuid nullable", "uuid", true, "String"},

		// Unknown types
		{"unknown type nullable", "unknowntype", true, "String"},
		{"unknown type not nullable", "unknowntype", false, "Option<String>"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := SQLToRust(tt.sqlType, tt.nullable)
			if got != tt.want {
				t.Errorf("SQLToRust(%q, %v) = %q, want %q", tt.sqlType, tt.nullable, got, tt.want)
			}
		})
	}
}

func TestJavaTypeMap(t *testing.T) {
	expectedMappings := map[string]string{
		"integer":          "Integer",
		"bigint":           "Long",
		"smallint":         "Short",
		"text":             "String",
		"boolean":          "Boolean",
		"double precision": "Double",
		"numeric":          "BigDecimal",
		"bytea":            "byte[]",
		"timestamp":        "Timestamp",
		"uuid":             "UUID",
		"date":             "Date",
	}

	for sqlType, expectedJavaType := range expectedMappings {
		if javaType, ok := JavaTypeMap[sqlType]; !ok {
			t.Errorf("JavaTypeMap missing entry for %q", sqlType)
		} else if javaType != expectedJavaType {
			t.Errorf("JavaTypeMap[%q] = %q, want %q", sqlType, javaType, expectedJavaType)
		}
	}

	if len(JavaTypeMap) == 0 {
		t.Error("JavaTypeMap is empty")
	}
}

func TestPHPTypeMap(t *testing.T) {
	expectedMappings := map[string]string{
		"integer":          "int",
		"bigint":           "int",
		"text":             "string",
		"boolean":          "bool",
		"double precision": "float",
		"bytea":            "string",
		"timestamp":        "\\DateTime",
		"uuid":             "string",
		"json":             "array",
	}

	for sqlType, expectedPHPType := range expectedMappings {
		if phpType, ok := PHPTypeMap[sqlType]; !ok {
			t.Errorf("PHPTypeMap missing entry for %q", sqlType)
		} else if phpType != expectedPHPType {
			t.Errorf("PHPTypeMap[%q] = %q, want %q", sqlType, phpType, expectedPHPType)
		}
	}

	if len(PHPTypeMap) == 0 {
		t.Error("PHPTypeMap is empty")
	}
}

func TestRustTypeMap(t *testing.T) {
	expectedMappings := map[string]string{
		"integer":          "i32",
		"bigint":           "i64",
		"smallint":         "i16",
		"text":             "String",
		"boolean":          "bool",
		"double precision": "f64",
		"real":             "f32",
		"bytea":            "Vec<u8>",
		"timestamp":        "NaiveDateTime",
		"timestamptz":      "DateTime<Utc>",
		"date":             "NaiveDate",
		"json":             "serde_json::Value",
	}

	for sqlType, expectedRustType := range expectedMappings {
		if rustType, ok := RustTypeMap[sqlType]; !ok {
			t.Errorf("RustTypeMap missing entry for %q", sqlType)
		} else if rustType != expectedRustType {
			t.Errorf("RustTypeMap[%q] = %q, want %q", sqlType, rustType, expectedRustType)
		}
	}

	if len(RustTypeMap) == 0 {
		t.Error("RustTypeMap is empty")
	}
}
