// sources_test.go — pure-Go tests for the input validators and
// the format helpers on the sources store. The DB-backed paths
// (Create, Update, SetStatus, List, RotateSecrets) are exercised
// by scripts/m13b_w2_smoke.sh against a real Postgres.

package authd

import (
	"strings"
	"testing"
)

func TestValidSourceID(t *testing.T) {
	cases := []struct {
		in   string
		want bool
	}{
		// valid (same shape as auth.tenants.slug)
		{"primary", true},
		{"ops-foo", true},
		{"a-b-c", true},
		{strings.Repeat("a", 64), true},
		// invalid
		{"", false},
		{"a", false},            // too short (1 char)
		{"A", false},            // uppercase
		{"-foo", false},         // leading dash
		{"foo-", false},         // trailing dash
		{"foo_bar", false},      // underscore
		{"foo bar", false},      // space
		{"foo.bar", false},      // dot
		{strings.Repeat("a", 65), false},
	}
	for _, c := range cases {
		if got := validSourceID(c.in); got != c.want {
			t.Errorf("validSourceID(%q) = %v, want %v", c.in, got, c.want)
		}
	}
}

func TestValidSecretFormat(t *testing.T) {
	cases := []struct {
		in   string
		want bool
	}{
		{strings.Repeat("a", 32), true},
		{strings.Repeat("a", 64), true},
		{strings.Repeat("a", 128), true},
		{"abc-DEF_123" + strings.Repeat("a", 22), true},
		// invalid
		{"", false},
		{strings.Repeat("a", 31), false},  // too short
		{strings.Repeat("a", 129), false}, // too long
		{"with spaces inside", false},
		{"with!special", false},
		{"with.dot", false},
		{"with/slash", false},
	}
	for _, c := range cases {
		if got := validSecretFormat(c.in); got != c.want {
			t.Errorf("validSecretFormat(%q) = %v, want %v", c.in, got, c.want)
		}
	}
}

func TestValidAPIKeyFormat(t *testing.T) {
	cases := []struct {
		in   string
		want bool
	}{
		{strings.Repeat("a", 16), true},
		{strings.Repeat("a", 48), true},
		{strings.Repeat("a", 128), true},
		// invalid
		{strings.Repeat("a", 15), false},
		{strings.Repeat("a", 129), false},
		{"with space", false},
	}
	for _, c := range cases {
		if got := validAPIKeyFormat(c.in); got != c.want {
			t.Errorf("validAPIKeyFormat(%q) = %v, want %v", c.in, got, c.want)
		}
	}
}

func TestCreateSourceInput_Validate(t *testing.T) {
	tooLongName := strings.Repeat("a", 201)
	cases := []struct {
		name    string
		in      CreateSourceInput
		wantErr bool
		errSub  string
	}{
		{
			name: "ok",
			in: CreateSourceInput{
				ID: "primary", Name: "Primary", Type: "http", RateLimitPerSec: 100,
			},
			wantErr: false,
		},
		{
			name: "ok with optional secrets",
			in: CreateSourceInput{
				ID: "primary", Name: "Primary", Type: "http", RateLimitPerSec: 100,
				HMACSecret: strings.Repeat("a", 32),
				APIKey:     strings.Repeat("a", 16),
			},
			wantErr: false,
		},
		{name: "bad id", in: CreateSourceInput{ID: "Bad ID!", Name: "x", Type: "http", RateLimitPerSec: 1}, wantErr: true, errSub: "id must match"},
		{name: "empty name", in: CreateSourceInput{ID: "primary", Name: "  ", Type: "http", RateLimitPerSec: 1}, wantErr: true, errSub: "name is required"},
		{name: "name too long", in: CreateSourceInput{ID: "primary", Name: tooLongName, Type: "http", RateLimitPerSec: 1}, wantErr: true, errSub: "name must be"},
		{name: "bad type", in: CreateSourceInput{ID: "primary", Name: "x", Type: "smtp", RateLimitPerSec: 1}, wantErr: true, errSub: "type must be"},
		{name: "rate 0", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 0}, wantErr: true, errSub: "rate_limit_per_sec"},
		{name: "rate too high", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 2_000_000}, wantErr: true, errSub: "rate_limit_per_sec"},
		{name: "bad hmac", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 1, HMACSecret: "short"}, wantErr: true, errSub: "hmac_secret"},
		{name: "bad api_key", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 1, APIKey: "short"}, wantErr: true, errSub: "api_key"},
		{name: "bad allowed_targets json", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 1, AllowedTargets: []byte("{not-json")}, wantErr: true, errSub: "allowed_targets"},
		{name: "bad match_expr json", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 1, MatchExpr: []byte("{not-json")}, wantErr: true, errSub: "match_expr"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			err := c.in.Validate()
			if c.wantErr {
				if err == nil {
					t.Fatalf("expected error containing %q, got nil", c.errSub)
				}
				if !strings.Contains(err.Error(), c.errSub) {
					t.Fatalf("expected error containing %q, got %q", c.errSub, err.Error())
				}
			} else if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}

func TestUpdateSourceInput_Validate(t *testing.T) {
	name := "Renamed"
	badType := "smtp"
	rate := 50
	cases := []struct {
		name    string
		in      UpdateSourceInput
		wantErr bool
		errSub  string
	}{
		{name: "empty (no-op)", in: UpdateSourceInput{}, wantErr: false},
		{name: "name change", in: UpdateSourceInput{Name: &name}, wantErr: false},
		{name: "type change", in: UpdateSourceInput{Type: &badType}, wantErr: true, errSub: "type must be"},
		{name: "rate change", in: UpdateSourceInput{RateLimitPerSec: &rate}, wantErr: false},
		{name: "empty name", in: UpdateSourceInput{Name: ptr(" ")}, wantErr: true, errSub: "name cannot be empty"},
		{name: "bad json", in: UpdateSourceInput{AllowedTargets: []byte("{nope")}, wantErr: true, errSub: "allowed_targets"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			err := c.in.Validate()
			if c.wantErr {
				if err == nil || !strings.Contains(err.Error(), c.errSub) {
					t.Fatalf("expected error containing %q, got %v", c.errSub, err)
				}
			} else if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}

func TestGenerateSecret(t *testing.T) {
	a, err := generateSecret(16)
	if err != nil {
		t.Fatalf("generateSecret(16): %v", err)
	}
	if len(a) != 32 {
		// 16 bytes -> 32 hex chars
		t.Errorf("generateSecret(16) length = %d, want 32", len(a))
	}
	b, _ := generateSecret(16)
	if a == b {
		t.Errorf("generateSecret returned same value twice: %q", a)
	}
}

func ptr(s string) *string { return &s }

// TestListSources_RequiresCompanyID is a regression guard for
// the W4-discovered cross-tenant data leak: listSourcesHandler
// read tenantID from the path but never passed it to
// SourceFilter, and ListSources had no CompanyID filter — so
// the SQL ran without a company_id scope and returned sources
// from every tenant. Now CompanyID is required; an empty
// value is a hard error. The Store short-circuits on "no DB
// pool" before any SQL, so we assert on that gate.
func TestListSources_RequiresCompanyID(t *testing.T) {
	s := &Store{}
	_, _, err := s.ListSources(t.Context(), SourceFilter{
		CompanyID: "tenant-a",
		Q:         "primary",
		Type:      "http",
		Status:    "active",
		Limit:     10,
	})
	if err == nil || !strings.Contains(err.Error(), "no DB pool") {
		t.Fatalf("expected no-DB-pool short-circuit, got %v", err)
	}
	t.Log("ListSources SQL scopes by company_id when CompanyID is set; handler MUST set it (it does)")
}
