// Patial Tech.
// Author, Ankit Patial

package pgm

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/jackc/pgx/v5"
)

// TestGetPoolNotInitialized verifies that GetPool panics when pool is not initialized
func TestGetPoolNotInitialized(t *testing.T) {
	// Save current pool state
	currentPool := poolPGX.Load()

	// Clear the pool to simulate uninitialized state
	poolPGX.Store(nil)

	// Restore pool after test
	defer func() {
		poolPGX.Store(currentPool)
	}()

	// Verify panic occurs
	defer func() {
		if r := recover(); r == nil {
			t.Error("GetPool should panic when pool not initialized")
		} else {
			// Check panic message
			msg, ok := r.(string)
			if !ok || !strings.Contains(msg, "not initialized") {
				t.Errorf("Expected panic message about initialization, got: %v", r)
			}
		}
	}()

	GetPool() // Should panic
}

// TestConfigValidation tests that InitPool validates configuration
func TestConfigValidation(t *testing.T) {
	tests := []struct {
		name      string
		config    Config
		wantPanic bool
		panicMsg  string
	}{
		{
			name: "empty connection string",
			config: Config{
				ConnString: "",
			},
			wantPanic: true,
			panicMsg:  "connection string is empty",
		},
		{
			name: "MinConns greater than MaxConns",
			config: Config{
				ConnString: "postgres://localhost/test",
				MaxConns:   5,
				MinConns:   10,
			},
			wantPanic: true,
			panicMsg:  "MinConns",
		},
		{
			name: "negative MaxConns",
			config: Config{
				ConnString: "postgres://localhost/test",
				MaxConns:   -1,
			},
			wantPanic: true,
			panicMsg:  "negative",
		},
		{
			name: "negative MinConns",
			config: Config{
				ConnString: "postgres://localhost/test",
				MinConns:   -1,
			},
			wantPanic: true,
			panicMsg:  "negative",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			defer func() {
				r := recover()
				if tt.wantPanic && r == nil {
					t.Errorf("InitPool should panic for %s", tt.name)
				}
				if !tt.wantPanic && r != nil {
					t.Errorf("InitPool should not panic for %s, got: %v", tt.name, r)
				}
				if tt.wantPanic && r != nil {
					msg := ""
					switch v := r.(type) {
					case string:
						msg = v
					case error:
						msg = v.Error()
					}
					if !strings.Contains(msg, tt.panicMsg) {
						t.Errorf("Expected panic message to contain %q, got: %q", tt.panicMsg, msg)
					}
				}
			}()

			InitPool(tt.config)
		})
	}
}

// TestClosePoolIdempotent verifies ClosePool can be called multiple times safely
func TestClosePoolIdempotent(t *testing.T) {
	// Save current pool
	currentPool := poolPGX.Load()
	defer func() {
		poolPGX.Store(currentPool)
	}()

	// Set to nil
	poolPGX.Store(nil)

	// Should not panic when called on nil pool
	ClosePool()
	ClosePool()
	ClosePool()

	// Should work fine - no panic expected
}

// TestIsNotFound tests the error checking utility
func TestIsNotFound(t *testing.T) {
	// Test with pgx.ErrNoRows
	if !IsNotFound(pgx.ErrNoRows) {
		t.Error("IsNotFound should return true for pgx.ErrNoRows")
	}

	// Test with other errors
	otherErr := errors.New("some other error")
	if IsNotFound(otherErr) {
		t.Error("IsNotFound should return false for non-ErrNoRows errors")
	}

	// Test with nil
	if IsNotFound(nil) {
		t.Error("IsNotFound should return false for nil")
	}
}

// TestPgTime tests PostgreSQL timestamp conversion
func TestPgTime(t *testing.T) {
	now := time.Now()
	pgTime := PgTime(now)

	if !pgTime.Valid {
		t.Error("PgTime should return valid timestamp")
	}

	if !pgTime.Time.Equal(now) {
		t.Errorf("PgTime time mismatch: got %v, want %v", pgTime.Time, now)
	}
}

// TestPgTimeNow tests current time conversion
func TestPgTimeNow(t *testing.T) {
	before := time.Now()
	pgTime := PgTimeNow()
	after := time.Now()

	if !pgTime.Valid {
		t.Error("PgTimeNow should return valid timestamp")
	}

	// Check that time is between before and after
	if pgTime.Time.Before(before) || pgTime.Time.After(after) {
		t.Error("PgTimeNow should return current time")
	}
}

// TestTsQueryFunctions tests full-text search query builders
func TestTsQueryFunctions(t *testing.T) {
	tests := []struct {
		name     string
		fn       func(string) string
		input    string
		expected string
	}{
		{
			name:     "TsAndQuery",
			fn:       TsAndQuery,
			input:    "hello world",
			expected: "hello & world",
		},
		{
			name:     "TsPrefixAndQuery",
			fn:       TsPrefixAndQuery,
			input:    "hello world",
			expected: "hello:* & world:*",
		},
		{
			name:     "TsOrQuery",
			fn:       TsOrQuery,
			input:    "hello world",
			expected: "hello | world",
		},
		{
			name:     "TsPrefixOrQuery",
			fn:       TsPrefixOrQuery,
			input:    "hello world",
			expected: "hello:* | world:*",
		},
		{
			name:     "TsAndQuery with multiple words",
			fn:       TsAndQuery,
			input:    "one two three",
			expected: "one & two & three",
		},
		{
			name:     "TsOrQuery with multiple words",
			fn:       TsOrQuery,
			input:    "one two three",
			expected: "one | two | three",
		},
		{
			name:     "TsAndQuery with extra spaces",
			fn:       TsAndQuery,
			input:    "hello    world",
			expected: "hello & world",
		},
	}

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

// TestFieldsWithSuffix tests the internal suffix function
func TestFieldsWithSuffix(t *testing.T) {
	result := fieldsWithSufix("hello world", ":*")
	expected := []string{"hello:*", "world:*"}

	if len(result) != len(expected) {
		t.Fatalf("fieldsWithSufix length mismatch: got %d, want %d", len(result), len(expected))
	}

	for i, v := range result {
		if v != expected[i] {
			t.Errorf("fieldsWithSufix[%d] = %q, want %q", i, v, expected[i])
		}
	}
}

// TestBeginTxErrorWrapping tests that BeginTx preserves error context
func TestBeginTxErrorWrapping(t *testing.T) {
	// This test verifies basic behavior without requiring a database connection
	// Actual error wrapping would require a failing database connection

	// Skip if no pool initialized (unit test environment)
	if poolPGX.Load() == nil {
		t.Skip("Skipping BeginTx test - requires initialized pool")
	}

	// Just verify the function accepts a context
	// In a real scenario with a cancelled context, it would return an error
	ctx := context.Background()

	// We can't fully test without a real DB connection, so just document behavior
	_, err := BeginTx(ctx)
	if err != nil {
		// Error is expected in test environment without valid DB
		t.Logf("BeginTx returned error (expected in test env): %v", err)
	}
}

// TestContextCancellation tests that cancelled context is handled
func TestContextCancellation(t *testing.T) {
	// Create an already-cancelled context
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	// Note: This test would require an actual database connection to verify
	// that the context cancellation is properly propagated. We're testing
	// that the context is accepted by the function signature.

	// The actual behavior would be tested in integration tests
	// For now, we just verify the function doesn't panic with cancelled context

	// Skip if no pool initialized (unit test environment)
	if poolPGX.Load() == nil {
		t.Skip("Skipping context cancellation test - requires initialized pool")
	}

	// This would return an error about cancelled context in real scenario
	_, err := BeginTx(ctx)
	if err == nil {
		t.Log("BeginTx with cancelled context - error expected in real scenario")
	}
}

// TestErrorTypes verifies exported error variables
func TestErrorTypes(t *testing.T) {
	if ErrConnStringMissing == nil {
		t.Error("ErrConnStringMissing should be defined")
	}

	if ErrInitTX == nil {
		t.Error("ErrInitTX should be defined")
	}

	if ErrCommitTX == nil {
		t.Error("ErrCommitTX should be defined")
	}

	if ErrNoRows == nil {
		t.Error("ErrNoRows should be defined")
	}

	// Check error messages are descriptive
	if !strings.Contains(ErrConnStringMissing.Error(), "connection string") {
		t.Error("ErrConnStringMissing should mention connection string")
	}
}

// TestStringPoolGetPut tests the string builder pool
func TestStringPoolGetPut(t *testing.T) {
	// Get a string builder from pool
	sb1 := getSB()
	if sb1 == nil {
		t.Fatal("getSB should return non-nil string builder")
	}

	// Use it
	sb1.WriteString("test")
	if sb1.String() != "test" {
		t.Error("String builder should work normally")
	}

	// Put it back
	putSB(sb1)

	// Get another one - should be reset
	sb2 := getSB()
	if sb2 == nil {
		t.Fatal("getSB should return non-nil string builder")
	}

	// Should be empty after reset
	if sb2.Len() != 0 {
		t.Error("String builder from pool should be reset")
	}

	putSB(sb2)
}

// TestConditionActionTypes tests condition action enum
func TestConditionActionTypes(t *testing.T) {
	// Verify enum values are distinct
	actions := []CondAction{
		CondActionNothing,
		CondActionNeedToClose,
		CondActionSubQuery,
	}

	seen := make(map[CondAction]bool)
	for _, action := range actions {
		if seen[action] {
			t.Errorf("Duplicate CondAction value: %v", action)
		}
		seen[action] = true
	}
}

// TestSelectQueryBuilderBasics tests basic query building
func TestSelectQueryBuilderBasics(t *testing.T) {
	// Create a test table
	testTable := &Table{Name: "users", FieldCount: 10}
	idField := Field("users.id")
	emailField := Field("users.email")

	// Test basic select
	qry := testTable.Select(idField, emailField)
	sql := qry.String()

	if !strings.Contains(sql, "SELECT") {
		t.Error("Query should contain SELECT")
	}
	if !strings.Contains(sql, "users.id") {
		t.Error("Query should contain users.id field")
	}
	if !strings.Contains(sql, "users.email") {
		t.Error("Query should contain users.email field")
	}
	if !strings.Contains(sql, "FROM users") {
		t.Error("Query should contain FROM users")
	}
}

// TestWhereConditionAccumulation tests that WHERE conditions accumulate
func TestWhereConditionAccumulation(t *testing.T) {
	testTable := &Table{Name: "users", FieldCount: 10}
	idField := Field("users.id")
	statusField := Field("users.status")

	// Create a query and add multiple WHERE conditions
	qry := testTable.Select(idField).
		Where(idField.Eq(1)).
		Where(statusField.Eq("active"))

	sql := qry.String()

	// Both conditions should be present
	if !strings.Contains(sql, "WHERE") {
		t.Error("Query should contain WHERE clause")
	}

	// Should have multiple conditions (this demonstrates accumulation)
	if strings.Count(sql, "$") < 2 {
		t.Error("Query should have multiple parameterized values")
	}
}

// TestInsertQueryBuilder tests insert query building
func TestInsertQueryBuilder(t *testing.T) {
	testTable := &Table{Name: "users", FieldCount: 10}

	qry := testTable.Insert()

	sql := qry.String()

	if !strings.Contains(sql, "INSERT INTO users") {
		t.Error("Query should contain INSERT INTO users")
	}
}

// TestUpdateQueryBuilder tests update query building
func TestUpdateQueryBuilder(t *testing.T) {
	testTable := &Table{Name: "users", FieldCount: 10}
	idField := Field("users.id")

	qry := testTable.Update().
		Where(idField.Eq(1))

	sql := qry.String()

	if !strings.Contains(sql, "UPDATE users") {
		t.Error("Query should contain UPDATE users")
	}
	if !strings.Contains(sql, "WHERE") {
		t.Error("Query should contain WHERE clause")
	}
}

// TestDeleteQueryBuilder tests delete query building
func TestDeleteQueryBuilder(t *testing.T) {
	testTable := &Table{Name: "users", FieldCount: 10}
	idField := Field("users.id")

	qry := testTable.Delete().Where(idField.Eq(1))

	sql := qry.String()

	if !strings.Contains(sql, "DELETE FROM users") {
		t.Error("Query should contain DELETE FROM users")
	}
	if !strings.Contains(sql, "WHERE") {
		t.Error("Query should contain WHERE clause")
	}
}

// TestFieldConditioners tests various field condition methods
func TestFieldConditioners(t *testing.T) {
	field := Field("users.age")

	tests := []struct {
		name      string
		condition Conditioner
		checkFunc func(string) bool
	}{
		{
			name:      "Eq",
			condition: field.Eq(25),
			checkFunc: func(s string) bool { return strings.Contains(s, " = $") },
		},
		{
			name:      "NotEq",
			condition: field.NotEq(25),
			checkFunc: func(s string) bool { return strings.Contains(s, " != $") },
		},
		{
			name:      "Gt",
			condition: field.Gt(25),
			checkFunc: func(s string) bool { return strings.Contains(s, " > $") },
		},
		{
			name:      "Lt",
			condition: field.Lt(25),
			checkFunc: func(s string) bool { return strings.Contains(s, " < $") },
		},
		{
			name:      "Gte",
			condition: field.Gte(25),
			checkFunc: func(s string) bool { return strings.Contains(s, " >= $") },
		},
		{
			name:      "Lte",
			condition: field.Lte(25),
			checkFunc: func(s string) bool { return strings.Contains(s, " <= $") },
		},
		{
			name:      "IsNull",
			condition: field.IsNull(),
			checkFunc: func(s string) bool { return strings.Contains(s, " IS NULL") },
		},
		{
			name:      "IsNotNull",
			condition: field.IsNotNull(),
			checkFunc: func(s string) bool { return strings.Contains(s, " IS NOT NULL") },
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Build a simple query to see the condition
			testTable := &Table{Name: "users", FieldCount: 10}
			qry := testTable.Select(field).Where(tt.condition)
			sql := qry.String()

			if !tt.checkFunc(sql) {
				t.Errorf("%s condition not properly rendered in SQL: %s", tt.name, sql)
			}
		})
	}
}

// TestFieldFunctions tests SQL function wrappers
func TestFieldFunctions(t *testing.T) {
	field := Field("users.count")

	tests := []struct {
		name     string
		result   Field
		contains string
	}{
		{
			name:     "Count",
			result:   field.Count(),
			contains: "COUNT(",
		},
		{
			name:     "Sum",
			result:   field.Sum(),
			contains: "SUM(",
		},
		{
			name:     "Avg",
			result:   field.Avg(),
			contains: "AVG(",
		},
		{
			name:     "Max",
			result:   field.Max(),
			contains: "MAX(",
		},
		{
			name:     "Min",
			result:   field.Min(),
			contains: "Min(",
		},
		{
			name:     "Lower",
			result:   field.Lower(),
			contains: "LOWER(",
		},
		{
			name:     "Upper",
			result:   field.Upper(),
			contains: "UPPER(",
		},
		{
			name:     "Trim",
			result:   field.Trim(),
			contains: "TRIM(",
		},
		{
			name:     "Asc",
			result:   field.Asc(),
			contains: " ASC",
		},
		{
			name:     "Desc",
			result:   field.Desc(),
			contains: " DESC",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resultStr := string(tt.result)
			if !strings.Contains(resultStr, tt.contains) {
				t.Errorf("%s should contain %q, got: %s", tt.name, tt.contains, resultStr)
			}
		})
	}
}

// TestInsertQueryValidation tests that Insert queries validate fields are set
func TestInsertQueryValidation(t *testing.T) {
	testTable := &Table{Name: "users", FieldCount: 10}
	idField := Field("users.id")

	t.Run("Exec without fields", func(t *testing.T) {
		qry := testTable.Insert()
		err := qry.Exec(context.Background())
		if err == nil {
			t.Error("Insert.Exec() should return error when no fields set")
		}
		if !strings.Contains(err.Error(), "no fields to insert") {
			t.Errorf("Expected error about no fields, got: %v", err)
		}
	})

	t.Run("ExecTx without fields", func(t *testing.T) {
		qry := testTable.Insert()
		// We can't test ExecTx without a real transaction, but we can test the error is defined
		err := qry.ExecTx(context.Background(), nil)
		if err == nil {
			t.Error("Insert.ExecTx() should return error when no fields set")
		}
		if !strings.Contains(err.Error(), "no fields to insert") {
			t.Errorf("Expected error about no fields, got: %v", err)
		}
	})

	t.Run("First without fields", func(t *testing.T) {
		qry := testTable.Insert().Returning(idField)
		var id int
		err := qry.First(context.Background(), &id)
		if err == nil {
			t.Error("Insert.First() should return error when no fields set")
		}
		if !strings.Contains(err.Error(), "no fields to insert") {
			t.Errorf("Expected error about no fields, got: %v", err)
		}
	})

	t.Run("FirstTx without fields", func(t *testing.T) {
		qry := testTable.Insert().Returning(idField)
		var id int
		err := qry.FirstTx(context.Background(), nil, &id)
		if err == nil {
			t.Error("Insert.FirstTx() should return error when no fields set")
		}
		if !strings.Contains(err.Error(), "no fields to insert") {
			t.Errorf("Expected error about no fields, got: %v", err)
		}
	})
}
