package domain_test

import (
	"testing"
	"time"

	"github.com/orchard9/rdev/internal/domain"
)

// =============================================================================
// APIKey Tests
// =============================================================================

func TestAPIKey_IsExpired(t *testing.T) {
	tests := []struct {
		name      string
		expiresAt *time.Time
		want      bool
	}{
		{
			name:      "nil expiration never expires",
			expiresAt: nil,
			want:      false,
		},
		{
			name:      "future expiration not expired",
			expiresAt: timePtr(time.Now().Add(time.Hour)),
			want:      false,
		},
		{
			name:      "past expiration is expired",
			expiresAt: timePtr(time.Now().Add(-time.Hour)),
			want:      true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			key := &domain.APIKey{ExpiresAt: tt.expiresAt}
			if got := key.IsExpired(); got != tt.want {
				t.Errorf("IsExpired() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAPIKey_IsRevoked(t *testing.T) {
	tests := []struct {
		name      string
		revokedAt *time.Time
		want      bool
	}{
		{
			name:      "nil revocation not revoked",
			revokedAt: nil,
			want:      false,
		},
		{
			name:      "set revocation is revoked",
			revokedAt: timePtr(time.Now()),
			want:      true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			key := &domain.APIKey{RevokedAt: tt.revokedAt}
			if got := key.IsRevoked(); got != tt.want {
				t.Errorf("IsRevoked() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAPIKey_IsActive(t *testing.T) {
	now := time.Now()
	tests := []struct {
		name      string
		expiresAt *time.Time
		revokedAt *time.Time
		want      bool
	}{
		{
			name:      "active when no expiration and not revoked",
			expiresAt: nil,
			revokedAt: nil,
			want:      true,
		},
		{
			name:      "active when future expiration and not revoked",
			expiresAt: timePtr(now.Add(time.Hour)),
			revokedAt: nil,
			want:      true,
		},
		{
			name:      "inactive when expired",
			expiresAt: timePtr(now.Add(-time.Hour)),
			revokedAt: nil,
			want:      false,
		},
		{
			name:      "inactive when revoked",
			expiresAt: nil,
			revokedAt: timePtr(now),
			want:      false,
		},
		{
			name:      "inactive when both expired and revoked",
			expiresAt: timePtr(now.Add(-time.Hour)),
			revokedAt: timePtr(now),
			want:      false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			key := &domain.APIKey{
				ExpiresAt: tt.expiresAt,
				RevokedAt: tt.revokedAt,
			}
			if got := key.IsActive(); got != tt.want {
				t.Errorf("IsActive() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAPIKey_HasScope(t *testing.T) {
	tests := []struct {
		name   string
		scopes []domain.Scope
		check  domain.Scope
		want   bool
	}{
		{
			name:   "empty scopes has nothing",
			scopes: []domain.Scope{},
			check:  domain.ScopeProjectsRead,
			want:   false,
		},
		{
			name:   "exact match",
			scopes: []domain.Scope{domain.ScopeProjectsRead},
			check:  domain.ScopeProjectsRead,
			want:   true,
		},
		{
			name:   "no match",
			scopes: []domain.Scope{domain.ScopeProjectsRead},
			check:  domain.ScopeProjectsExecute,
			want:   false,
		},
		{
			name:   "admin grants any scope",
			scopes: []domain.Scope{domain.ScopeAdmin},
			check:  domain.ScopeProjectsExecute,
			want:   true,
		},
		{
			name:   "multiple scopes match",
			scopes: []domain.Scope{domain.ScopeProjectsRead, domain.ScopeProjectsExecute},
			check:  domain.ScopeProjectsExecute,
			want:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			key := &domain.APIKey{Scopes: tt.scopes}
			if got := key.HasScope(tt.check); got != tt.want {
				t.Errorf("HasScope(%v) = %v, want %v", tt.check, got, tt.want)
			}
		})
	}
}

func TestAPIKey_HasAnyScope(t *testing.T) {
	tests := []struct {
		name   string
		scopes []domain.Scope
		check  []domain.Scope
		want   bool
	}{
		{
			name:   "empty check returns false",
			scopes: []domain.Scope{domain.ScopeProjectsRead},
			check:  []domain.Scope{},
			want:   false,
		},
		{
			name:   "matches first scope",
			scopes: []domain.Scope{domain.ScopeProjectsRead},
			check:  []domain.Scope{domain.ScopeProjectsRead, domain.ScopeProjectsExecute},
			want:   true,
		},
		{
			name:   "matches second scope",
			scopes: []domain.Scope{domain.ScopeProjectsExecute},
			check:  []domain.Scope{domain.ScopeProjectsRead, domain.ScopeProjectsExecute},
			want:   true,
		},
		{
			name:   "no match",
			scopes: []domain.Scope{domain.ScopeKeysWrite},
			check:  []domain.Scope{domain.ScopeProjectsRead, domain.ScopeProjectsExecute},
			want:   false,
		},
		{
			name:   "admin matches any",
			scopes: []domain.Scope{domain.ScopeAdmin},
			check:  []domain.Scope{domain.ScopeProjectsRead},
			want:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			key := &domain.APIKey{Scopes: tt.scopes}
			if got := key.HasAnyScope(tt.check...); got != tt.want {
				t.Errorf("HasAnyScope(%v) = %v, want %v", tt.check, got, tt.want)
			}
		})
	}
}

func TestAPIKey_HasProjectAccess(t *testing.T) {
	tests := []struct {
		name       string
		scopes     []domain.Scope
		projectIDs []domain.ProjectID
		checkID    domain.ProjectID
		want       bool
	}{
		{
			name:       "nil project list grants all access",
			scopes:     []domain.Scope{domain.ScopeProjectsRead},
			projectIDs: nil,
			checkID:    "proj-1",
			want:       true,
		},
		{
			name:       "admin grants all access",
			scopes:     []domain.Scope{domain.ScopeAdmin},
			projectIDs: []domain.ProjectID{"proj-1"},
			checkID:    "proj-2",
			want:       true,
		},
		{
			name:       "explicit project in list",
			scopes:     []domain.Scope{domain.ScopeProjectsRead},
			projectIDs: []domain.ProjectID{"proj-1", "proj-2"},
			checkID:    "proj-1",
			want:       true,
		},
		{
			name:       "project not in list",
			scopes:     []domain.Scope{domain.ScopeProjectsRead},
			projectIDs: []domain.ProjectID{"proj-1", "proj-2"},
			checkID:    "proj-3",
			want:       false,
		},
		{
			name:       "empty project list denies all",
			scopes:     []domain.Scope{domain.ScopeProjectsRead},
			projectIDs: []domain.ProjectID{},
			checkID:    "proj-1",
			want:       false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			key := &domain.APIKey{
				Scopes:     tt.scopes,
				ProjectIDs: tt.projectIDs,
			}
			if got := key.HasProjectAccess(tt.checkID); got != tt.want {
				t.Errorf("HasProjectAccess(%v) = %v, want %v", tt.checkID, got, tt.want)
			}
		})
	}
}

func TestAPIKey_IsIPAllowed(t *testing.T) {
	tests := []struct {
		name       string
		allowedIPs []string
		clientIP   string
		want       bool
	}{
		{
			name:       "nil allowed IPs allows all",
			allowedIPs: nil,
			clientIP:   "192.168.1.100",
			want:       true,
		},
		{
			name:       "empty allowed IPs allows all",
			allowedIPs: []string{},
			clientIP:   "192.168.1.100",
			want:       true,
		},
		{
			name:       "exact IP match",
			allowedIPs: []string{"192.168.1.100"},
			clientIP:   "192.168.1.100",
			want:       true,
		},
		{
			name:       "exact IP no match",
			allowedIPs: []string{"192.168.1.100"},
			clientIP:   "192.168.1.101",
			want:       false,
		},
		{
			name:       "CIDR match",
			allowedIPs: []string{"192.168.1.0/24"},
			clientIP:   "192.168.1.100",
			want:       true,
		},
		{
			name:       "CIDR no match",
			allowedIPs: []string{"192.168.1.0/24"},
			clientIP:   "192.168.2.100",
			want:       false,
		},
		{
			name:       "multiple CIDRs first match",
			allowedIPs: []string{"10.0.0.0/8", "192.168.0.0/16"},
			clientIP:   "10.1.2.3",
			want:       true,
		},
		{
			name:       "multiple CIDRs second match",
			allowedIPs: []string{"10.0.0.0/8", "192.168.0.0/16"},
			clientIP:   "192.168.5.10",
			want:       true,
		},
		{
			name:       "multiple CIDRs no match",
			allowedIPs: []string{"10.0.0.0/8", "192.168.0.0/16"},
			clientIP:   "172.16.0.1",
			want:       false,
		},
		{
			name:       "IPv6 CIDR match",
			allowedIPs: []string{"2001:db8::/32"},
			clientIP:   "2001:db8::1",
			want:       true,
		},
		{
			name:       "IPv6 CIDR no match",
			allowedIPs: []string{"2001:db8::/32"},
			clientIP:   "2001:db9::1",
			want:       false,
		},
		{
			name:       "mixed IPv4 and IPv6",
			allowedIPs: []string{"192.168.1.0/24", "2001:db8::/32"},
			clientIP:   "2001:db8::100",
			want:       true,
		},
		{
			name:       "invalid client IP",
			allowedIPs: []string{"192.168.1.0/24"},
			clientIP:   "not-an-ip",
			want:       false,
		},
		{
			name:       "single IP with /32 CIDR",
			allowedIPs: []string{"192.168.1.100/32"},
			clientIP:   "192.168.1.100",
			want:       true,
		},
		{
			name:       "single IP with /32 CIDR no match",
			allowedIPs: []string{"192.168.1.100/32"},
			clientIP:   "192.168.1.101",
			want:       false,
		},
		{
			name:       "localhost IPv4",
			allowedIPs: []string{"127.0.0.1"},
			clientIP:   "127.0.0.1",
			want:       true,
		},
		{
			name:       "localhost IPv6",
			allowedIPs: []string{"::1"},
			clientIP:   "::1",
			want:       true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			key := &domain.APIKey{AllowedIPs: tt.allowedIPs}
			if got := key.IsIPAllowed(tt.clientIP); got != tt.want {
				t.Errorf("IsIPAllowed(%q) = %v, want %v", tt.clientIP, got, tt.want)
			}
		})
	}
}

// =============================================================================
// Helpers
// =============================================================================

func timePtr(t time.Time) *time.Time {
	return &t
}
