package database

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/go-sql-driver/mysql"
)

func TestNewConnectionContextRejectsMissingContextBeforeOpening(t *testing.T) {
	connection, err := NewConnectionContext(nil, PoolOptions{MaxOpenConns: 1, MaxIdleConns: 1})
	if err == nil || connection != nil {
		t.Fatalf("NewConnectionContext(nil) = (%v, %v)", connection, err)
	}
}

func TestNewConnectionContextAppliesCancellationToFirstProbe(t *testing.T) {
	t.Setenv("TIDB_HOST", "192.0.2.1")
	t.Setenv("TIDB_PORT", "4000")
	t.Setenv("TIDB_USER", "user")
	t.Setenv("TIDB_PASSWORD", "password")
	t.Setenv("TIDB_DATABASE", "nexuscrm")
	t.Setenv("TIDB_TLS_DISABLE", "1")
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	started := time.Now()
	connection, err := NewConnectionContext(ctx, PoolOptions{MaxOpenConns: 1, MaxIdleConns: 1})
	if err == nil || connection != nil {
		t.Fatalf("canceled NewConnectionContext = (%v, %v)", connection, err)
	}
	if elapsed := time.Since(started); elapsed > time.Second {
		t.Fatalf("first database probe ignored cancellation for %s", elapsed)
	}
}

func TestDefaultPoolOptionsReadsEnvOverrides(t *testing.T) {
	t.Setenv("NEXUSCRM_DB_MAX_OPEN_CONNS", "200")
	t.Setenv("NEXUSCRM_DB_MAX_IDLE_CONNS", "150")

	options := DefaultPoolOptions()

	if options.MaxOpenConns != 200 {
		t.Fatalf("MaxOpenConns = %d, want 200", options.MaxOpenConns)
	}
	if options.MaxIdleConns != 150 {
		t.Fatalf("MaxIdleConns = %d, want 150", options.MaxIdleConns)
	}
}

func TestDefaultPoolOptionsCapsIdleToOpen(t *testing.T) {
	t.Setenv("NEXUSCRM_DB_MAX_OPEN_CONNS", "12")
	t.Setenv("NEXUSCRM_DB_MAX_IDLE_CONNS", "30")

	options := DefaultPoolOptions()

	if options.MaxOpenConns != 12 {
		t.Fatalf("MaxOpenConns = %d, want 12", options.MaxOpenConns)
	}
	if options.MaxIdleConns != 12 {
		t.Fatalf("MaxIdleConns = %d, want capped 12", options.MaxIdleConns)
	}
}

func TestOpenDBFromEnvAppliesPoolOptionsWithoutPing(t *testing.T) {
	t.Setenv("TIDB_HOST", "invalid-db-host-for-pool-options")
	t.Setenv("TIDB_USER", "user")
	t.Setenv("TIDB_PASSWORD", "password")
	t.Setenv("TIDB_DATABASE", "nexuscrm")
	t.Setenv("TIDB_TLS_DISABLE", "1")

	db, err := OpenDBFromEnv(PoolOptions{
		MaxOpenConns:    7,
		MaxIdleConns:    11,
		ConnMaxLifetime: 1,
		ConnMaxIdleTime: 1,
	})
	if err != nil {
		t.Fatalf("OpenDBFromEnv: %v", err)
	}
	defer db.Close()

	stats := db.Stats()
	if stats.MaxOpenConnections != 7 {
		t.Fatalf("MaxOpenConnections = %d, want 7", stats.MaxOpenConnections)
	}
	if stats.Idle > 7 {
		t.Fatalf("Idle = %d, want no more than MaxOpenConns", stats.Idle)
	}
}

func TestDBOpenSpecAppliesInterpolationAndNetworkTimeout(t *testing.T) {
	openSpec := dbOpenSpec{
		driverName: "mysql-tracing",
		dsn:        "user:password@tcp(127.0.0.1:4000)/nexuscrm?parseTime=true",
	}
	dsn, err := openSpec.dsnForOptions(PoolOptions{
		InterpolateParams: true,
		NetworkTimeout:    7 * time.Second,
	})
	if err != nil {
		t.Fatalf("dsnForOptions: %v", err)
	}
	config, err := mysql.ParseDSN(dsn)
	if err != nil {
		t.Fatalf("ParseDSN: %v", err)
	}
	if !config.InterpolateParams {
		t.Fatal("InterpolateParams = false, want true")
	}
	if config.Timeout != 7*time.Second || config.ReadTimeout != 7*time.Second || config.WriteTimeout != 7*time.Second {
		t.Fatalf("timeouts = (%s, %s, %s), want 7s", config.Timeout, config.ReadTimeout, config.WriteTimeout)
	}
}

func TestOpenIndependentDBUsesCapturedSpecAndNewBoundedPool(t *testing.T) {
	primary, err := sql.Open("mysql-tracing", "user:password@tcp(127.0.0.1:1)/nexuscrm?parseTime=true")
	if err != nil {
		t.Fatalf("sql.Open primary: %v", err)
	}
	t.Cleanup(func() { _ = primary.Close() })

	openSpec := dbOpenSpec{driverName: "mysql-tracing", dsn: "user:password@tcp(127.0.0.1:1)/nexuscrm?parseTime=true"}
	conn := newTiDBConnection(primary, openSpec)
	independent, err := conn.OpenIndependentDB(PoolOptions{
		MaxOpenConns:    1,
		MaxIdleConns:    1,
		ConnMaxLifetime: time.Minute,
		ConnMaxIdleTime: time.Minute,
	})
	if err != nil {
		t.Fatalf("OpenIndependentDB: %v", err)
	}
	t.Cleanup(func() { _ = independent.Close() })

	if independent == primary {
		t.Fatal("independent session DB reused the primary pool")
	}
	if got := independent.Stats().MaxOpenConnections; got != 1 {
		t.Fatalf("independent MaxOpenConnections = %d, want 1", got)
	}
}

func TestDBOpenSpecSnapshotDoesNotFollowLaterEnvironmentChanges(t *testing.T) {
	t.Setenv("TIDB_HOST", "captured.example.invalid")
	t.Setenv("TIDB_USER", "captured-user")
	t.Setenv("TIDB_PASSWORD", "captured-password")
	t.Setenv("TIDB_DATABASE", "captured-db")
	t.Setenv("TIDB_TLS_DISABLE", "1")

	openSpec := dbOpenSpecFromEnv()
	t.Setenv("TIDB_HOST", "later.example.invalid")
	t.Setenv("TIDB_DATABASE", "later-db")

	if !strings.Contains(openSpec.dsn, "captured.example.invalid") || !strings.Contains(openSpec.dsn, "/captured-db?") {
		t.Fatalf("captured DSN did not retain initial environment: %q", openSpec.dsn)
	}
	if strings.Contains(openSpec.dsn, "later.example.invalid") || strings.Contains(openSpec.dsn, "/later-db?") {
		t.Fatalf("captured DSN followed later environment: %q", openSpec.dsn)
	}
}

func TestNewConnectionFromDBCannotInventIndependentSessionConfiguration(t *testing.T) {
	primary, err := sql.Open("mysql-tracing", "user:password@tcp(127.0.0.1:1)/nexuscrm?parseTime=true")
	if err != nil {
		t.Fatalf("sql.Open primary: %v", err)
	}
	t.Cleanup(func() { _ = primary.Close() })

	conn := NewConnectionFromDB(primary)
	independent, err := conn.OpenIndependentDB(PoolOptions{MaxOpenConns: 1, MaxIdleConns: 1})
	if !errors.Is(err, ErrIndependentDBUnavailable) || independent != nil {
		t.Fatalf("OpenIndependentDB = (%v, %v), want nil and ErrIndependentDBUnavailable", independent, err)
	}
}
