package redisconn_test

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

	"galaxy/redisconn"

	"github.com/alicebob/miniredis/v2"
	"go.opentelemetry.io/otel/metric/noop"
	tracenoop "go.opentelemetry.io/otel/trace/noop"
)

func TestDefaultConfigReturnsExpectedTuning(t *testing.T) {
	t.Parallel()

	cfg := redisconn.DefaultConfig()
	if cfg.OperationTimeout != redisconn.DefaultOperationTimeout {
		t.Fatalf("operation timeout = %v, want %v", cfg.OperationTimeout, redisconn.DefaultOperationTimeout)
	}
	if cfg.DB != redisconn.DefaultDB {
		t.Fatalf("db = %d, want %d", cfg.DB, redisconn.DefaultDB)
	}
}

func TestConfigValidateRejectsInvalidValues(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		mutate  func(*redisconn.Config)
		wantSub string
	}{
		{
			name: "missing master",
			mutate: func(c *redisconn.Config) {
				c.MasterAddr = ""
			},
			wantSub: "master addr",
		},
		{
			name: "missing password",
			mutate: func(c *redisconn.Config) {
				c.Password = ""
			},
			wantSub: "password",
		},
		{
			name: "blank replica entry",
			mutate: func(c *redisconn.Config) {
				c.ReplicaAddrs = []string{"   "}
			},
			wantSub: "replica addr",
		},
		{
			name: "negative db",
			mutate: func(c *redisconn.Config) {
				c.DB = -1
			},
			wantSub: "db must not be negative",
		},
		{
			name: "non-positive timeout",
			mutate: func(c *redisconn.Config) {
				c.OperationTimeout = 0
			},
			wantSub: "operation timeout",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			cfg := redisconn.DefaultConfig()
			cfg.MasterAddr = "127.0.0.1:6379"
			cfg.Password = "secret"
			tt.mutate(&cfg)

			err := cfg.Validate()
			if err == nil {
				t.Fatalf("expected validate error, got nil")
			}
			if !strings.Contains(err.Error(), tt.wantSub) {
				t.Fatalf("error %q does not contain %q", err, tt.wantSub)
			}
		})
	}
}

func TestLoadFromEnvHappyPath(t *testing.T) {
	const prefix = "TESTSVC"
	t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379")
	t.Setenv(prefix+"_REDIS_REPLICA_ADDRS", "127.0.0.1:6380, 127.0.0.1:6381 ,")
	t.Setenv(prefix+"_REDIS_PASSWORD", "secret")
	t.Setenv(prefix+"_REDIS_DB", "3")
	t.Setenv(prefix+"_REDIS_OPERATION_TIMEOUT", "500ms")

	cfg, err := redisconn.LoadFromEnv(prefix)
	if err != nil {
		t.Fatalf("load from env: %v", err)
	}
	if cfg.MasterAddr != "127.0.0.1:6379" {
		t.Fatalf("master addr = %q", cfg.MasterAddr)
	}
	if cfg.Password != "secret" {
		t.Fatalf("password = %q", cfg.Password)
	}
	if got, want := cfg.DB, 3; got != want {
		t.Fatalf("db = %d, want %d", got, want)
	}
	if got, want := cfg.OperationTimeout, 500*time.Millisecond; got != want {
		t.Fatalf("operation timeout = %v, want %v", got, want)
	}
	if got, want := len(cfg.ReplicaAddrs), 2; got != want {
		t.Fatalf("replica count = %d, want %d", got, want)
	}
}

func TestLoadFromEnvRejectsDeprecatedTLSEnabled(t *testing.T) {
	const prefix = "TESTSVC"
	t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379")
	t.Setenv(prefix+"_REDIS_PASSWORD", "secret")
	t.Setenv(prefix+"_REDIS_TLS_ENABLED", "true")

	_, err := redisconn.LoadFromEnv(prefix)
	if err == nil {
		t.Fatal("expected error when TLS_ENABLED is set")
	}
	if !strings.Contains(err.Error(), "TLS_ENABLED") {
		t.Fatalf("error %q should name TLS_ENABLED", err)
	}
	if !strings.Contains(err.Error(), "ARCHITECTURE.md") {
		t.Fatalf("error %q should reference ARCHITECTURE.md", err)
	}
}

func TestLoadFromEnvRejectsDeprecatedUsername(t *testing.T) {
	const prefix = "TESTSVC"
	t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379")
	t.Setenv(prefix+"_REDIS_PASSWORD", "secret")
	t.Setenv(prefix+"_REDIS_USERNAME", "anything")

	_, err := redisconn.LoadFromEnv(prefix)
	if err == nil {
		t.Fatal("expected error when USERNAME is set")
	}
	if !strings.Contains(err.Error(), "USERNAME") {
		t.Fatalf("error %q should name USERNAME", err)
	}
}

func TestLoadFromEnvRequiresPassword(t *testing.T) {
	const prefix = "TESTSVC"
	t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379")
	t.Setenv(prefix+"_REDIS_PASSWORD", "")

	if _, err := redisconn.LoadFromEnv(prefix); err == nil {
		t.Fatal("expected error when password is empty")
	}
}

func TestNewMasterClientPingsMiniredis(t *testing.T) {
	t.Parallel()

	server := miniredis.RunT(t)
	server.RequireAuth("secret")

	cfg := redisconn.DefaultConfig()
	cfg.MasterAddr = server.Addr()
	cfg.Password = "secret"
	if err := cfg.Validate(); err != nil {
		t.Fatalf("validate: %v", err)
	}

	client := redisconn.NewMasterClient(cfg)
	t.Cleanup(func() {
		_ = client.Close()
	})

	if err := redisconn.Ping(context.Background(), client, cfg.OperationTimeout); err != nil {
		t.Fatalf("ping miniredis: %v", err)
	}
}

func TestNewReplicaClientsReturnsExpectedLength(t *testing.T) {
	t.Parallel()

	server1 := miniredis.RunT(t)
	server2 := miniredis.RunT(t)

	cfg := redisconn.DefaultConfig()
	cfg.MasterAddr = "ignored:6379"
	cfg.Password = "secret"
	cfg.ReplicaAddrs = []string{server1.Addr(), server2.Addr()}

	clients := redisconn.NewReplicaClients(cfg)
	t.Cleanup(func() {
		for _, client := range clients {
			_ = client.Close()
		}
	})

	if got, want := len(clients), 2; got != want {
		t.Fatalf("client count = %d, want %d", got, want)
	}
}

func TestNewReplicaClientsReturnsNilWhenUnconfigured(t *testing.T) {
	t.Parallel()

	cfg := redisconn.DefaultConfig()
	cfg.MasterAddr = "ignored:6379"
	cfg.Password = "secret"

	if clients := redisconn.NewReplicaClients(cfg); clients != nil {
		t.Fatalf("clients = %v, want nil", clients)
	}
}

func TestInstrumentAcceptsNoopProviders(t *testing.T) {
	t.Parallel()

	server := miniredis.RunT(t)
	server.RequireAuth("secret")

	cfg := redisconn.DefaultConfig()
	cfg.MasterAddr = server.Addr()
	cfg.Password = "secret"

	client := redisconn.NewMasterClient(cfg)
	t.Cleanup(func() {
		_ = client.Close()
	})

	err := redisconn.Instrument(
		client,
		redisconn.WithTracerProvider(tracenoop.NewTracerProvider()),
		redisconn.WithMeterProvider(noop.NewMeterProvider()),
	)
	if err != nil {
		t.Fatalf("instrument: %v", err)
	}

	if err := redisconn.Ping(context.Background(), client, cfg.OperationTimeout); err != nil {
		t.Fatalf("ping after instrument: %v", err)
	}
}

func TestInstrumentRejectsNilClient(t *testing.T) {
	t.Parallel()

	if err := redisconn.Instrument(nil); err == nil {
		t.Fatal("expected error for nil client")
	}
}
