package auth

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

	"github.com/go-gost/core/auth"
	"github.com/go-gost/x/internal/loader"
	xlogger "github.com/go-gost/x/logger"
)

// --- mock types ---

type mockLoader struct {
	loadData string
	loadErr  error
	closed   bool
}

func (m *mockLoader) Load(ctx context.Context) (io.Reader, error) {
	if m.loadErr != nil {
		return nil, m.loadErr
	}
	return strings.NewReader(m.loadData), nil
}

func (m *mockLoader) Close() error {
	m.closed = true
	return nil
}

type mockMapper struct {
	mapData map[string]string
	mapErr  error
	closed  bool
}

func (m *mockMapper) Load(ctx context.Context) (io.Reader, error) {
	return nil, nil
}

func (m *mockMapper) Map(ctx context.Context) (map[string]string, error) {
	if m.mapErr != nil {
		return nil, m.mapErr
	}
	return m.mapData, nil
}

func (m *mockMapper) Close() error {
	m.closed = true
	return nil
}

var _ loader.Loader = (*mockLoader)(nil)
var _ loader.Loader = (*mockMapper)(nil)
var _ loader.Mapper = (*mockMapper)(nil)

// --- option tests ---

func TestAuthsOption(t *testing.T) {
	opts := &options{}
	auths := map[string]string{"user": "pass"}
	AuthsOption(auths)(opts)
	if len(opts.auths) != 1 || opts.auths["user"] != "pass" {
		t.Fatalf("expected auths to be set, got %v", opts.auths)
	}
}

func TestReloadPeriodOption(t *testing.T) {
	opts := &options{}
	ReloadPeriodOption(5 * time.Second)(opts)
	if opts.period != 5*time.Second {
		t.Fatalf("expected period 5s, got %v", opts.period)
	}
}

func TestFileLoaderOption(t *testing.T) {
	opts := &options{}
	ml := &mockLoader{}
	FileLoaderOption(ml)(opts)
	if opts.fileLoader != ml {
		t.Fatal("expected fileLoader to be set")
	}
}

func TestRedisLoaderOption(t *testing.T) {
	opts := &options{}
	ml := &mockLoader{}
	RedisLoaderOption(ml)(opts)
	if opts.redisLoader != ml {
		t.Fatal("expected redisLoader to be set")
	}
}

func TestHTTPLoaderOption(t *testing.T) {
	opts := &options{}
	ml := &mockLoader{}
	HTTPLoaderOption(ml)(opts)
	if opts.httpLoader != ml {
		t.Fatal("expected httpLoader to be set")
	}
}

func TestLoggerOption(t *testing.T) {
	opts := &options{}
	l := xlogger.Nop()
	LoggerOption(l)(opts)
	if opts.logger != l {
		t.Fatal("expected logger to be set")
	}
}

// --- NewAuthenticator tests ---

func TestNewAuthenticator_Defaults(t *testing.T) {
	p := NewAuthenticator()
	if p == nil {
		t.Fatal("expected authenticator to be created")
	}
	defer p.(*authenticator).Close()

	ap := p.(*authenticator)
	if ap.logger == nil {
		t.Fatal("expected non-nil logger (Nop fallback)")
	}
	if ap.cancelFunc == nil {
		t.Fatal("expected cancelFunc to be set")
	}
	if len(ap.kvs) != 0 {
		t.Fatal("expected empty kvs")
	}
}

func TestNewAuthenticator_WithAuths(t *testing.T) {
	p := NewAuthenticator(AuthsOption(map[string]string{"u": "p"}))
	defer p.(*authenticator).Close()

	// Initial reload is async — wait for it.
	time.Sleep(50 * time.Millisecond)

	ap := p.(*authenticator)
	ap.mu.RLock()
	if ap.kvs["u"] != "p" {
		t.Fatal("expected kvs to contain u=p")
	}
	ap.mu.RUnlock()
}

func TestNewAuthenticator_WithLogger(t *testing.T) {
	l := xlogger.Nop()
	p := NewAuthenticator(LoggerOption(l))
	defer p.(*authenticator).Close()

	ap := p.(*authenticator)
	if ap.logger != l {
		t.Fatal("expected custom logger")
	}
}

// --- Authenticate tests ---

func TestAuthenticate_NilReceiver(t *testing.T) {
	var p *authenticator
	id, ok := p.Authenticate(context.Background(), "u", "p")
	if !ok || id != "" {
		t.Fatalf("nil receiver should return '', true, got %q, %v", id, ok)
	}
}

func TestAuthenticate_EmptyKVs(t *testing.T) {
	p := NewAuthenticator()
	defer p.(*authenticator).Close()

	id, ok := p.Authenticate(context.Background(), "u", "p")
	if ok || id != "" {
		t.Fatalf("empty kvs should return '', false, got %q, %v", id, ok)
	}
}

func TestAuthenticate_UserNotFound(t *testing.T) {
	p := NewAuthenticator(AuthsOption(map[string]string{"admin": "secret"}))
	defer p.(*authenticator).Close()

	// Wait for initial reload to populate kvs
	time.Sleep(50 * time.Millisecond)

	// When user is not in kvs, Authenticate returns (user, false)
	id, ok := p.Authenticate(context.Background(), "nobody", "")
	if ok || id != "nobody" {
		t.Fatalf("unknown user should return 'nobody', false, got %q, %v", id, ok)
	}
}

func TestAuthenticate_UserFoundNoPassword(t *testing.T) {
	p := NewAuthenticator(AuthsOption(map[string]string{"open": ""}))
	defer p.(*authenticator).Close()

	time.Sleep(50 * time.Millisecond)

	id, ok := p.Authenticate(context.Background(), "open", "")
	if !ok || id != "open" {
		t.Fatalf("expected open, true, got %q, %v", id, ok)
	}
	id2, ok2 := p.Authenticate(context.Background(), "open", "anything")
	if !ok2 || id2 != "open" {
		t.Fatalf("any password should pass for empty-password user, got %q, %v", id2, ok2)
	}
}

func TestAuthenticate_PasswordMatch(t *testing.T) {
	p := NewAuthenticator(AuthsOption(map[string]string{"admin": "secret"}))
	defer p.(*authenticator).Close()

	time.Sleep(50 * time.Millisecond)

	id, ok := p.Authenticate(context.Background(), "admin", "secret")
	if !ok || id != "admin" {
		t.Fatalf("expected admin, true, got %q, %v", id, ok)
	}
}

func TestAuthenticate_PasswordMismatch(t *testing.T) {
	p := NewAuthenticator(AuthsOption(map[string]string{"admin": "secret"}))
	defer p.(*authenticator).Close()

	time.Sleep(50 * time.Millisecond)

	id, ok := p.Authenticate(context.Background(), "admin", "wrong")
	if ok {
		t.Fatalf("wrong password should return false, got %q, %v", id, ok)
	}
}

// --- parseAuths tests ---

func TestParseAuths_NilReader(t *testing.T) {
	p := &authenticator{}
	result, err := p.parseAuths(nil)
	if err != nil || result != nil {
		t.Fatalf("nil reader should return nil, nil, got %v, %v", result, err)
	}
}

func TestParseAuths_EmptyReader(t *testing.T) {
	p := &authenticator{}
	result, err := p.parseAuths(strings.NewReader(""))
	if err != nil || len(result) != 0 {
		t.Fatalf("empty reader should return empty map, got %v, %v", result, err)
	}
}

func TestParseAuths_Comments(t *testing.T) {
	p := &authenticator{}
	result, err := p.parseAuths(strings.NewReader("# this is a comment\n# another comment"))
	if err != nil || len(result) != 0 {
		t.Fatalf("comments should be skipped, got %v, %v", result, err)
	}
}

func TestParseAuths_SingleKey(t *testing.T) {
	p := &authenticator{}
	result, err := p.parseAuths(strings.NewReader("testuser"))
	if err != nil {
		t.Fatal(err)
	}
	if result["testuser"] != "" {
		t.Fatalf("single key should have empty password, got %q", result["testuser"])
	}
}

func TestParseAuths_KeyValue(t *testing.T) {
	p := &authenticator{}
	result, err := p.parseAuths(strings.NewReader("testuser testpass"))
	if err != nil {
		t.Fatal(err)
	}
	if result["testuser"] != "testpass" {
		t.Fatalf("expected testpass, got %q", result["testuser"])
	}
}

func TestParseAuths_TabConversion(t *testing.T) {
	p := &authenticator{}
	result, err := p.parseAuths(strings.NewReader("testuser\ttestpass"))
	if err != nil {
		t.Fatal(err)
	}
	if result["testuser"] != "testpass" {
		t.Fatalf("tab should be converted to space, got %q", result["testuser"])
	}
}

func TestParseAuths_MultipleLines(t *testing.T) {
	p := &authenticator{}
	data := "user1 pass1\nuser2\nuser3 pass3\n# comment\n\n  user4  pass4  "
	result, err := p.parseAuths(strings.NewReader(data))
	if err != nil {
		t.Fatal(err)
	}
	if len(result) != 4 {
		t.Fatalf("expected 4 entries, got %d", len(result))
	}
	if result["user1"] != "pass1" {
		t.Fatalf("user1: expected pass1, got %q", result["user1"])
	}
	if result["user2"] != "" {
		t.Fatalf("user2: expected empty, got %q", result["user2"])
	}
	if result["user3"] != "pass3" {
		t.Fatalf("user3: expected pass3, got %q", result["user3"])
	}
	if result["user4"] != "pass4" {
		t.Fatalf("user4: expected pass4, got %q", result["user4"])
	}
}

func TestParseAuths_EmptyKeyInPair(t *testing.T) {
	p := &authenticator{}
	result, err := p.parseAuths(strings.NewReader("  extra  spaces  here"))
	if err != nil {
		t.Fatal(err)
	}
	if result["extra"] != "spaces  here" {
		t.Fatalf("expected 'spaces  here', got %q", result["extra"])
	}
}

func TestParseAuths_WhitespaceOnlyLines(t *testing.T) {
	// TrimSpace removes leading/trailing whitespace before SplitN,
	// so whitespace-only lines become empty and produce no entries.
	p := &authenticator{}
	result, err := p.parseAuths(strings.NewReader("   \n\t  "))
	if err != nil || len(result) != 0 {
		t.Fatalf("whitespace-only lines should produce empty map, got %v, %v", result, err)
	}
}

// --- load tests ---

func TestLoad_NoLoaders(t *testing.T) {
	p := &authenticator{logger: xlogger.Nop()}
	m, err := p.load(context.Background())
	if err != nil || len(m) != 0 {
		t.Fatalf("expected empty map, nil err, got %v, %v", m, err)
	}
}

func TestLoad_FileLoaderMapper_Success(t *testing.T) {
	p := &authenticator{
		options: options{
			fileLoader: &mockMapper{mapData: map[string]string{"a": "b"}},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err != nil || m["a"] != "b" {
		t.Fatalf("expected a=b, got %v, %v", m, err)
	}
}

func TestLoad_FileLoaderMapper_Error(t *testing.T) {
	p := &authenticator{
		options: options{
			fileLoader: &mockMapper{mapErr: errors.New("map error")},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err == nil {
		t.Fatal("expected error from mapper")
	}
}

func TestLoad_FileLoaderNonMapper_Success(t *testing.T) {
	p := &authenticator{
		options: options{
			fileLoader: &mockLoader{loadData: "u1 p1"},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err != nil || m["u1"] != "p1" {
		t.Fatalf("expected u1=p1, got %v, %v", m, err)
	}
}

func TestLoad_FileLoaderNonMapper_LoadError(t *testing.T) {
	p := &authenticator{
		options: options{
			fileLoader: &mockLoader{loadErr: errors.New("load error")},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err == nil {
		t.Fatal("expected error from loader")
	}
}

func TestLoad_FileLoaderNonMapper_EmptyData(t *testing.T) {
	// mockLoader with empty loadData returns empty string reader;
	// parseAuths succeeds with empty result (not a parse error).
	p := &authenticator{
		options: options{
			fileLoader: &mockLoader{},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err != nil || len(m) != 0 {
		t.Fatalf("expected empty map, got %v, %v", m, err)
	}
}

func TestLoad_RedisLoaderMapper_Success(t *testing.T) {
	p := &authenticator{
		options: options{
			redisLoader: &mockMapper{mapData: map[string]string{"r": "s"}},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err != nil || m["r"] != "s" {
		t.Fatalf("expected r=s, got %v, %v", m, err)
	}
}

func TestLoad_RedisLoaderMapper_Error(t *testing.T) {
	p := &authenticator{
		options: options{
			redisLoader: &mockMapper{mapErr: errors.New("redis error")},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err == nil {
		t.Fatal("expected error from redis mapper")
	}
}

func TestLoad_RedisLoaderNonMapper_Success(t *testing.T) {
	p := &authenticator{
		options: options{
			redisLoader: &mockLoader{loadData: "r1 rp1"},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err != nil || m["r1"] != "rp1" {
		t.Fatalf("expected r1=rp1, got %v, %v", m, err)
	}
}

func TestLoad_RedisLoaderNonMapper_LoadError(t *testing.T) {
	p := &authenticator{
		options: options{
			redisLoader: &mockLoader{loadErr: errors.New("redis load err")},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestLoad_RedisLoaderNonMapper_ParseError(t *testing.T) {
	p := &authenticator{
		options: options{
			redisLoader: &mockLoader{loadData: "# comment"},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	// parseAuths succeeds (comments are skipped), just empty
	if err != nil || len(m) != 0 {
		t.Fatalf("expected empty map, got %v, %v", m, err)
	}
}

func TestLoad_HTTPLoader_Success(t *testing.T) {
	p := &authenticator{
		options: options{
			httpLoader: &mockLoader{loadData: "h1 hp1"},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err != nil || m["h1"] != "hp1" {
		t.Fatalf("expected h1=hp1, got %v, %v", m, err)
	}
}

func TestLoad_HTTPLoader_LoadError(t *testing.T) {
	p := &authenticator{
		options: options{
			httpLoader: &mockLoader{loadErr: errors.New("http error")},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err == nil {
		t.Fatal("expected error from http loader")
	}
}

func TestLoad_HTTPLoader_ParseError(t *testing.T) {
	p := &authenticator{
		options: options{
			httpLoader: &mockLoader{loadData: "# comment"},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err != nil || len(m) != 0 {
		t.Fatalf("expected empty map, got %v, %v", m, err)
	}
}

func TestLoad_MultipleLoaders_Merge(t *testing.T) {
	p := &authenticator{
		options: options{
			fileLoader:  &mockLoader{loadData: "f1 fp1"},
			redisLoader: &mockLoader{loadData: "r1 rp1"},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if m["f1"] != "fp1" || m["r1"] != "rp1" {
		t.Fatalf("expected merged results, got %v", m)
	}
}

func TestLoad_FirstSuccessSecondError(t *testing.T) {
	// fileLoader succeeds, redisLoader fails — error from redisLoader is returned
	// but fileLoader results are preserved
	p := &authenticator{
		options: options{
			fileLoader:  &mockLoader{loadData: "f1 fp1"},
			redisLoader: &mockLoader{loadErr: errors.New("redis fail")},
		},
		logger: xlogger.Nop(),
	}
	m, err := p.load(context.Background())
	if err == nil {
		t.Fatal("expected error from redis loader")
	}
	if m["f1"] != "fp1" {
		t.Fatalf("file loader results should be preserved, got %v", m)
	}
}

func TestLoad_RedisMapper_ErrorConditionalLoadErr(t *testing.T) {
	// redisLoader mapper error when loadErr is already set (from fileLoader error)
	p := &authenticator{
		options: options{
			fileLoader:  &mockLoader{loadErr: errors.New("file fail")},
			redisLoader: &mockMapper{mapErr: errors.New("redis fail")},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	// loadErr was set by fileLoader, redisLoader error doesn't overwrite it
	if err == nil || err.Error() != "file fail" {
		t.Fatalf("expected 'file fail', got %v", err)
	}
}

func TestLoad_HTTPLoader_ErrorConditionalLoadErr(t *testing.T) {
	// httpLoader error when loadErr already set
	p := &authenticator{
		options: options{
			fileLoader: &mockLoader{loadErr: errors.New("file fail")},
			httpLoader: &mockLoader{loadErr: errors.New("http fail")},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err == nil || err.Error() != "file fail" {
		t.Fatalf("expected 'file fail' (first error preserved), got %v", err)
	}
}

func TestLoad_FileMapper_ErrorConditionalLoadErr(t *testing.T) {
	// fileLoader mapper error, then redisLoader non-mapper error
	// fileLoader mapper sets loadErr, redisLoader non-mapper doesn't overwrite (conditional)
	p := &authenticator{
		options: options{
			fileLoader:  &mockMapper{mapErr: errors.New("file map fail")},
			redisLoader: &mockLoader{loadErr: errors.New("redis fail")},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err == nil || err.Error() != "file map fail" {
		t.Fatalf("expected 'file map fail', got %v", err)
	}
}

// --- reload tests ---

func TestReload_Success(t *testing.T) {
	p := &authenticator{
		options: options{
			auths: map[string]string{"static": "val"},
		},
		kvs:    make(map[string]string),
		logger: xlogger.Nop(),
	}
	err := p.reload(context.Background())
	if err != nil || len(p.kvs) != 1 || p.kvs["static"] != "val" {
		t.Fatalf("expected static=val, got %v, err=%v", p.kvs, err)
	}
}

func TestReload_LoadError(t *testing.T) {
	p := &authenticator{
		options: options{
			fileLoader: &mockLoader{loadErr: errors.New("fail")},
		},
		kvs:    map[string]string{"old": "data"},
		logger: xlogger.Nop(),
	}
	err := p.reload(context.Background())
	if err == nil {
		t.Fatal("expected error from reload")
	}
	// On error, kvs should not be updated (old data preserved)
	if p.kvs["old"] != "data" {
		t.Fatal("old kvs should be preserved on reload error")
	}
}

func TestReload_ClearsOldData(t *testing.T) {
	p := &authenticator{
		options: options{
			auths: map[string]string{"new": "val"},
		},
		kvs:    map[string]string{"old": "data"},
		logger: xlogger.Nop(),
	}
	err := p.reload(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if _, exists := p.kvs["old"]; exists {
		t.Fatal("old data should be cleared on successful reload")
	}
	if p.kvs["new"] != "val" {
		t.Fatal("new data should be present")
	}
}

// --- periodReload tests ---

func TestPeriodReload_NoPeriod(t *testing.T) {
	p := &authenticator{
		options: options{
			auths: map[string]string{"k": "v"},
		},
		kvs:    make(map[string]string),
		logger: xlogger.Nop(),
	}
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	err := p.periodReload(ctx)
	if err != nil {
		t.Fatalf("expected nil error for zero period, got %v", err)
	}
	// With period <= 0, the initial reload happened and then returned
	if p.kvs["k"] != "v" {
		t.Fatal("expected k=v after initial reload")
	}
}

func TestPeriodReload_ShortPeriod_Clamped(t *testing.T) {
	p := &authenticator{
		options: options{
			period: 500 * time.Millisecond, // < 1s, gets clamped to 1s
		},
		kvs:    make(map[string]string),
		logger: xlogger.Nop(),
	}
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	// Initial reload happens, then ticker starts but ctx cancels before first tick
	err := p.periodReload(ctx)
	if err == nil {
		t.Fatal("expected context error")
	}
}

func TestPeriodReload_ContextCancellation(t *testing.T) {
	p := &authenticator{
		options: options{
			period: 10 * time.Second, // long period so tick doesn't fire
		},
		kvs:    make(map[string]string),
		logger: xlogger.Nop(),
	}
	ctx, cancel := context.WithCancel(context.Background())

	done := make(chan error, 1)
	go func() {
		done <- p.periodReload(ctx)
	}()

	// Give it time to complete the initial reload and enter the loop
	time.Sleep(50 * time.Millisecond)
	cancel()

	err := <-done
	if err == nil {
		t.Fatal("expected context error")
	}
}

func TestPeriodReload_ReloadError_Initial(t *testing.T) {
	p := &authenticator{
		options: options{
			fileLoader: &mockLoader{loadErr: errors.New("fail")},
			period:     0, // no period, just initial reload
		},
		kvs:    make(map[string]string),
		logger: xlogger.Nop(),
	}
	err := p.periodReload(context.Background())
	// period is 0, so after initial reload it returns nil
	if err != nil {
		t.Fatalf("period 0 should return nil, got %v", err)
	}
}

// --- Close tests ---

func TestClose_NoLoaders(t *testing.T) {
	p := NewAuthenticator()
	err := p.(*authenticator).Close()
	if err != nil {
		t.Fatal(err)
	}
}

func TestClose_WithLoaders(t *testing.T) {
	fl := &mockLoader{}
	rl := &mockLoader{}
	hl := &mockLoader{}
	p := NewAuthenticator(
		FileLoaderOption(fl),
		RedisLoaderOption(rl),
		HTTPLoaderOption(hl),
	)
	err := p.(*authenticator).Close()
	if err != nil {
		t.Fatal(err)
	}
	if !fl.closed || !rl.closed || !hl.closed {
		t.Fatal("all loaders should be closed")
	}
}

func TestClose_CallsCancelFunc(t *testing.T) {
	p := &authenticator{
		kvs:    make(map[string]string),
		logger: xlogger.Nop(),
	}
	ctx, cancel := context.WithCancel(context.Background())
	p.cancelFunc = cancel

	err := p.Close()
	if err != nil {
		t.Fatal(err)
	}
	// Verify context was cancelled
	select {
	case <-ctx.Done():
		// expected
	default:
		t.Fatal("context should be cancelled after Close")
	}
}

// --- AuthenticatorGroup tests ---

func TestAuthenticatorGroup_Empty(t *testing.T) {
	g := AuthenticatorGroup()
	id, ok := g.Authenticate(context.Background(), "u", "p")
	if ok || id != "" {
		t.Fatalf("empty group should return '', false, got %q, %v", id, ok)
	}
}

func TestAuthenticatorGroup_SingleSuccess(t *testing.T) {
	g := AuthenticatorGroup(&staticAuther{id: "user1", ok: true})
	id, ok := g.Authenticate(context.Background(), "u", "p")
	if !ok || id != "user1" {
		t.Fatalf("expected user1, true, got %q, %v", id, ok)
	}
}

func TestAuthenticatorGroup_SingleFail(t *testing.T) {
	g := AuthenticatorGroup(&staticAuther{id: "", ok: false})
	id, ok := g.Authenticate(context.Background(), "u", "p")
	if ok || id != "" {
		t.Fatalf("expected '', false, got %q, %v", id, ok)
	}
}

func TestAuthenticatorGroup_Fallthrough(t *testing.T) {
	g := AuthenticatorGroup(
		&staticAuther{id: "", ok: false},
		&staticAuther{id: "second", ok: true},
	)
	id, ok := g.Authenticate(context.Background(), "u", "p")
	if !ok || id != "second" {
		t.Fatalf("expected second, true, got %q, %v", id, ok)
	}
}

func TestAuthenticatorGroup_NilEntries(t *testing.T) {
	g := AuthenticatorGroup(
		nil,
		&staticAuther{id: "third", ok: true},
	)
	id, ok := g.Authenticate(context.Background(), "u", "p")
	if !ok || id != "third" {
		t.Fatalf("nil entries should be skipped, expected third, true, got %q, %v", id, ok)
	}
}

func TestAuthenticatorGroup_AllFail(t *testing.T) {
	g := AuthenticatorGroup(
		&staticAuther{id: "", ok: false},
		&staticAuther{id: "", ok: false},
	)
	id, ok := g.Authenticate(context.Background(), "u", "p")
	if ok || id != "" {
		t.Fatalf("all fail should return '', false, got %q, %v", id, ok)
	}
}

func TestAuthenticatorGroup_Creation(t *testing.T) {
	a1 := &staticAuther{id: "a", ok: true}
	a2 := &staticAuther{id: "b", ok: false}
	g := AuthenticatorGroup(a1, a2).(*authenticatorGroup)
	if len(g.authers) != 2 {
		t.Fatalf("expected 2 authers, got %d", len(g.authers))
	}
}

// --- scanner error tests ---

type errorReader struct{}

func (e *errorReader) Read(p []byte) (int, error) {
	return 0, errors.New("read error")
}

func TestParseAuths_ScannerError(t *testing.T) {
	p := &authenticator{}
	_, err := p.parseAuths(&errorReader{})
	if err == nil {
		t.Fatal("expected scanner error")
	}
}

// --- load parse error branch tests ---

func TestLoad_FileLoaderNonMapper_ParseErrorBranch(t *testing.T) {
	// File loader non-mapper: Load succeeds but reader triggers scanner error in parseAuths.
	// This hits the "file loader parse" Warnf branch.
	// parse errors are NOT propagated as loadErr — only load errors are.
	p := &authenticator{
		options: options{
			fileLoader: &errorLoader{reader: &errorReader{}},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err != nil {
		t.Fatalf("parse error should not propagate, got %v", err)
	}
}

func TestLoad_RedisLoaderNonMapper_ParseErrorBranch(t *testing.T) {
	// Redis loader non-mapper — parseAuths returns scanner error.
	p := &authenticator{
		options: options{
			redisLoader: &errorLoader{reader: &errorReader{}},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err != nil {
		t.Fatalf("parse error should not propagate, got %v", err)
	}
}

func TestLoad_HTTPLoader_ParseErrorBranch(t *testing.T) {
	// HTTP loader — parseAuths returns scanner error.
	p := &authenticator{
		options: options{
			httpLoader: &errorLoader{reader: &errorReader{}},
		},
		logger: xlogger.Nop(),
	}
	_, err := p.load(context.Background())
	if err != nil {
		t.Fatalf("parse error should not propagate, got %v", err)
	}
}

type errorLoader struct {
	reader io.Reader
}

func (l *errorLoader) Load(ctx context.Context) (io.Reader, error) {
	return l.reader, nil
}

func (l *errorLoader) Close() error { return nil }

var _ loader.Loader = (*errorLoader)(nil)

// --- periodReload ticker fire test ---

func TestPeriodReload_TickerFires(t *testing.T) {
	// Set up with a loader that succeeds on initial reload, so we enter the tick loop.
	// Use period=1s so ticker fires quickly.
	p := &authenticator{
		options: options{
			period:     1 * time.Second,
			fileLoader: &mockLoader{loadData: "k v"},
		},
		kvs:    make(map[string]string),
		logger: xlogger.Nop(),
	}

	ctx, cancel := context.WithTimeout(context.Background(), 1100*time.Millisecond)
	defer cancel()

	done := make(chan error, 1)
	go func() {
		done <- p.periodReload(ctx)
	}()

	err := <-done
	if err == nil {
		t.Fatal("expected context deadline exceeded")
	}

	// kvs should be populated by the initial reload
	p.mu.RLock()
	v := p.kvs["k"]
	p.mu.RUnlock()
	if v != "v" {
		t.Fatalf("expected k=v after initial reload, got %q", v)
	}
}

func TestPeriodReload_TickerReloadError(t *testing.T) {
	// Loader that fails — covers the Warnf inside the ticker.C case.
	// Initial reload fails (Warnf line 116), ticker fires, reload fails again (Warnf line 133).
	p := &authenticator{
		options: options{
			period:     1 * time.Second,
			fileLoader: &mockLoader{loadErr: errors.New("persistent fail")},
		},
		kvs:    make(map[string]string),
		logger: xlogger.Nop(),
	}

	ctx, cancel := context.WithTimeout(context.Background(), 1100*time.Millisecond)
	defer cancel()

	done := make(chan error, 1)
	go func() {
		done <- p.periodReload(ctx)
	}()

	err := <-done
	if err == nil {
		t.Fatal("expected context deadline exceeded")
	}
}

// --- helper type for group tests ---

type staticAuther struct {
	id string
	ok bool
}

func (s *staticAuther) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
	return s.id, s.ok
}
