package api

import (
	"net/http/httptest"
	"sync"
	"testing"
	"time"
)

// fixedTimeForTest returns a fixed time for deterministic testing
func fixedTimeForTest() time.Time {
	return time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
}

func resetTrustedProxyConfig() {
	trustedProxyCIDRs = nil
	trustedProxyOnce = sync.Once{}
}

func TestGetClientIPRejectsSpoofedLoopback(t *testing.T) {
	t.Setenv("PULSE_TRUSTED_PROXY_CIDRS", "")
	resetTrustedProxyConfig()

	req := httptest.NewRequest("GET", "/", nil)
	req.RemoteAddr = "198.51.100.42:54321"
	req.Header.Set("X-Forwarded-For", "127.0.0.1")

	if got := GetClientIP(req); got != "198.51.100.42" {
		t.Fatalf("expected remote IP when proxy is untrusted, got %q", got)
	}
}

func TestGetClientIPUsesForwardedForTrustedProxy(t *testing.T) {
	t.Setenv("PULSE_TRUSTED_PROXY_CIDRS", "127.0.0.1/32")
	resetTrustedProxyConfig()

	req := httptest.NewRequest("GET", "/", nil)
	req.RemoteAddr = "127.0.0.1:54321"
	req.Header.Set("X-Forwarded-For", "203.0.113.44")

	if got := GetClientIP(req); got != "203.0.113.44" {
		t.Fatalf("expected forwarded IP for trusted proxy, got %q", got)
	}
}

func TestIsPrivateIP(t *testing.T) {
	t.Helper()
	cases := []struct {
		name string
		ip   string
		want bool
	}{
		{"public IPv4", "198.51.100.42", false},
		{"private IPv4", "10.1.2.3", true},
		{"loopback IPv4", "127.0.0.1", true},
		{"link-local IPv6", "fe80::1", true},
		{"loopback IPv6 with port", "[::1]:8443", true},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			if got := isPrivateIP(tc.ip); got != tc.want {
				t.Fatalf("isPrivateIP(%q) = %v, want %v", tc.ip, got, tc.want)
			}
		})
	}
}

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

	tests := []struct {
		name            string
		ip              string
		trustedNetworks []string
		expected        bool
	}{
		// Nil trusted networks - falls back to private IP check
		{
			name:            "private IP trusted when no networks configured",
			ip:              "10.0.0.5",
			trustedNetworks: nil,
			expected:        true,
		},
		{
			name:            "public IP untrusted when no networks configured",
			ip:              "198.51.100.42",
			trustedNetworks: nil,
			expected:        false,
		},
		// Empty trusted networks slice - falls back to private IP check
		{
			name:            "private IP trusted with empty networks",
			ip:              "192.168.1.1",
			trustedNetworks: []string{},
			expected:        true,
		},
		// Custom CIDR networks
		{
			name:            "IP within custom CIDR trusted",
			ip:              "203.0.113.44:8080",
			trustedNetworks: []string{"203.0.113.0/24"},
			expected:        true,
		},
		{
			name:            "IP outside custom CIDR untrusted",
			ip:              "198.51.100.42",
			trustedNetworks: []string{"203.0.113.0/24"},
			expected:        false,
		},
		// Edge cases - empty/invalid input
		{
			name:            "empty IP string returns false",
			ip:              "",
			trustedNetworks: []string{"10.0.0.0/8"},
			expected:        false,
		},
		{
			name:            "invalid IP returns false",
			ip:              "not-an-ip",
			trustedNetworks: []string{"10.0.0.0/8"},
			expected:        false,
		},
		// Invalid CIDR is skipped, not matched
		{
			name:            "invalid CIDR in list is skipped",
			ip:              "10.0.0.5",
			trustedNetworks: []string{"invalid-cidr", "10.0.0.0/8"},
			expected:        true,
		},
		{
			name:            "only invalid CIDRs returns false",
			ip:              "10.0.0.5",
			trustedNetworks: []string{"invalid-cidr", "also-invalid"},
			expected:        false,
		},
		// Whitespace handling in CIDRs
		{
			name:            "CIDR with whitespace is trimmed",
			ip:              "10.0.0.5",
			trustedNetworks: []string{"  10.0.0.0/8  "},
			expected:        true,
		},
		// Multiple valid CIDRs
		{
			name:            "IP matches second CIDR",
			ip:              "172.16.5.10",
			trustedNetworks: []string{"10.0.0.0/8", "172.16.0.0/12"},
			expected:        true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			result := isTrustedNetwork(tt.ip, tt.trustedNetworks)
			if result != tt.expected {
				t.Errorf("isTrustedNetwork(%q, %v) = %v, want %v", tt.ip, tt.trustedNetworks, result, tt.expected)
			}
		})
	}
}

func TestExtractRemoteIP(t *testing.T) {
	cases := []struct {
		name       string
		remoteAddr string
		want       string
	}{
		// Empty input
		{"empty string", "", ""},

		// IPv4 with port
		{"IPv4 with port", "192.168.1.100:54321", "192.168.1.100"},
		{"localhost with port", "127.0.0.1:8080", "127.0.0.1"},
		{"public IP with port", "203.0.113.44:443", "203.0.113.44"},

		// IPv4 without port
		{"IPv4 without port", "192.168.1.100", "192.168.1.100"},
		{"localhost without port", "127.0.0.1", "127.0.0.1"},

		// IPv6 with port (bracketed)
		{"IPv6 loopback with port", "[::1]:8080", "::1"},
		{"IPv6 full with port", "[2001:db8::1]:443", "2001:db8::1"},
		{"IPv6 link-local with port", "[fe80::1]:8080", "fe80::1"},

		// IPv6 without port (bracketed)
		{"IPv6 loopback bracketed", "[::1]", "::1"},
		{"IPv6 full bracketed", "[2001:db8::1]", "2001:db8::1"},

		// IPv6 without brackets (raw)
		{"IPv6 loopback raw", "::1", "::1"},
		{"IPv6 full raw", "2001:db8::1", "2001:db8::1"},

		// Edge cases
		{"port only", ":8080", ""},
		{"brackets only", "[]", ""},
		{"whitespace", "  ", "  "},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := extractRemoteIP(tc.remoteAddr)
			if got != tc.want {
				t.Errorf("extractRemoteIP(%q) = %q, want %q", tc.remoteAddr, got, tc.want)
			}
		})
	}
}

func TestFirstValidForwardedIP(t *testing.T) {
	cases := []struct {
		name   string
		header string
		want   string
	}{
		// Empty input
		{"empty string", "", ""},
		{"whitespace only", "   ", ""},

		// Single IP
		{"single IPv4", "192.168.1.100", "192.168.1.100"},
		{"single IPv4 with whitespace", "  192.168.1.100  ", "192.168.1.100"},
		{"single IPv6", "2001:db8::1", "2001:db8::1"},
		{"single IPv6 bracketed", "[2001:db8::1]", "2001:db8::1"},
		{"single IPv6 loopback", "::1", "::1"},

		// Multiple IPs (comma-separated)
		{"two IPs first valid", "192.168.1.100, 10.0.0.1", "192.168.1.100"},
		{"two IPs with spaces", "  192.168.1.100  ,  10.0.0.1  ", "192.168.1.100"},
		{"three IPs", "203.0.113.1, 10.0.0.1, 172.16.0.1", "203.0.113.1"},

		// Invalid first, valid second
		{"invalid first then valid", "not-an-ip, 192.168.1.100", "192.168.1.100"},
		{"empty first then valid", ", 192.168.1.100", "192.168.1.100"},
		{"garbage then valid", "garbage, foobar, 10.0.0.1", "10.0.0.1"},

		// All invalid
		{"all invalid", "not-an-ip, also-invalid", ""},
		{"hostnames not IPs", "example.com, localhost", ""},

		// Mixed IPv4 and IPv6
		{"IPv6 first then IPv4", "2001:db8::1, 192.168.1.1", "2001:db8::1"},
		{"IPv4 first then IPv6", "192.168.1.1, 2001:db8::1", "192.168.1.1"},

		// Edge cases
		{"IP with port rejected", "192.168.1.100:8080", ""},
		{"bracketed IPv6 with port rejected", "[2001:db8::1]:443", ""},
		{"multiple commas", "192.168.1.1,,,10.0.0.1", "192.168.1.1"},
		{"only commas", ",,,", ""},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := firstValidForwardedIP(tc.header)
			if got != tc.want {
				t.Errorf("firstValidForwardedIP(%q) = %q, want %q", tc.header, got, tc.want)
			}
		})
	}
}

func TestIsPrivateIPExtended(t *testing.T) {
	// Extended test cases beyond the basic ones in TestIsPrivateIP
	cases := []struct {
		name string
		ip   string
		want bool
	}{
		// RFC 1918 private ranges
		{"10.x.x.x range start", "10.0.0.0", true},
		{"10.x.x.x range middle", "10.128.64.32", true},
		{"10.x.x.x range end", "10.255.255.255", true},
		{"172.16-31.x.x start", "172.16.0.0", true},
		{"172.16-31.x.x middle", "172.24.128.64", true},
		{"172.16-31.x.x end", "172.31.255.255", true},
		{"172.15.x.x outside range", "172.15.255.255", false},
		{"172.32.x.x outside range", "172.32.0.0", false},
		{"192.168.x.x start", "192.168.0.0", true},
		{"192.168.x.x end", "192.168.255.255", true},
		{"192.169.x.x outside range", "192.169.0.0", false},

		// Loopback
		{"loopback start", "127.0.0.0", true},
		{"loopback middle", "127.128.64.32", true},
		{"loopback end", "127.255.255.255", true},

		// IPv6 private/local
		{"IPv6 loopback", "::1", true},
		{"IPv6 unique local fc00::/7 start", "fc00::1", true},
		{"IPv6 unique local fd00::", "fd00::1234", true},
		{"IPv6 link-local fe80::/10", "fe80::abcd:1234", true},

		// Public IPs
		{"Google DNS", "8.8.8.8", false},
		{"Cloudflare DNS", "1.1.1.1", false},
		{"documentation range 192.0.2.x", "192.0.2.1", false},
		{"documentation range 198.51.100.x", "198.51.100.1", false},
		{"documentation range 203.0.113.x", "203.0.113.1", false},
		{"IPv6 public", "2001:4860:4860::8888", false},

		// With ports
		{"private with port", "192.168.1.1:8080", true},
		{"public with port", "8.8.8.8:53", false},
		{"IPv6 loopback with port", "[::1]:443", true},
		{"IPv6 public with port", "[2001:4860:4860::8888]:443", false},

		// Invalid inputs
		{"empty string", "", false},
		{"invalid IP", "not-an-ip", false},
		{"hostname", "example.com", false},
		{"partial IP", "192.168", false},
		{"IPv4 with extra octet", "192.168.1.1.1", false},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := isPrivateIP(tc.ip)
			if got != tc.want {
				t.Errorf("isPrivateIP(%q) = %v, want %v", tc.ip, got, tc.want)
			}
		})
	}
}

// resetFailedLogins clears the failed login state for testing
func resetFailedLogins() {
	failedMu.Lock()
	defer failedMu.Unlock()
	failedLogins = make(map[string]*FailedLogin)
}

// resetSessionTracking clears session tracking state for testing
func resetSessionTracking() {
	sessionsMu.Lock()
	defer sessionsMu.Unlock()
	allSessions = make(map[string][]string)
}

func TestRecordFailedLogin(t *testing.T) {
	resetFailedLogins()

	t.Run("increments count on each failure", func(t *testing.T) {
		resetFailedLogins()
		identifier := "test-user-1"

		RecordFailedLogin(identifier)
		attempts, _, _ := GetLockoutInfo(identifier)
		if attempts != 1 {
			t.Errorf("attempts = %d, want 1", attempts)
		}

		RecordFailedLogin(identifier)
		attempts, _, _ = GetLockoutInfo(identifier)
		if attempts != 2 {
			t.Errorf("attempts = %d, want 2", attempts)
		}

		RecordFailedLogin(identifier)
		attempts, _, _ = GetLockoutInfo(identifier)
		if attempts != 3 {
			t.Errorf("attempts = %d, want 3", attempts)
		}
	})

	t.Run("triggers lockout at max attempts", func(t *testing.T) {
		resetFailedLogins()
		identifier := "test-user-2"

		// Record up to max failed attempts
		for i := 0; i < maxFailedAttempts; i++ {
			RecordFailedLogin(identifier)
		}

		attempts, lockedUntil, isLocked := GetLockoutInfo(identifier)
		if attempts != maxFailedAttempts {
			t.Errorf("attempts = %d, want %d", attempts, maxFailedAttempts)
		}
		if !isLocked {
			t.Error("expected isLocked = true")
		}
		if lockedUntil.IsZero() {
			t.Error("expected lockedUntil to be set")
		}
	})

	t.Run("independent identifiers", func(t *testing.T) {
		resetFailedLogins()
		identifier1 := "user-a"
		identifier2 := "user-b"

		RecordFailedLogin(identifier1)
		RecordFailedLogin(identifier1)
		RecordFailedLogin(identifier2)

		attempts1, _, _ := GetLockoutInfo(identifier1)
		attempts2, _, _ := GetLockoutInfo(identifier2)

		if attempts1 != 2 {
			t.Errorf("identifier1 attempts = %d, want 2", attempts1)
		}
		if attempts2 != 1 {
			t.Errorf("identifier2 attempts = %d, want 1", attempts2)
		}
	})
}

func TestClearFailedLogins(t *testing.T) {
	resetFailedLogins()

	t.Run("clears failed login count", func(t *testing.T) {
		resetFailedLogins()
		identifier := "test-user-clear"

		RecordFailedLogin(identifier)
		RecordFailedLogin(identifier)

		attempts, _, _ := GetLockoutInfo(identifier)
		if attempts != 2 {
			t.Errorf("attempts before clear = %d, want 2", attempts)
		}

		ClearFailedLogins(identifier)

		attempts, _, _ = GetLockoutInfo(identifier)
		if attempts != 0 {
			t.Errorf("attempts after clear = %d, want 0", attempts)
		}
	})

	t.Run("clearing nonexistent identifier does not panic", func(t *testing.T) {
		resetFailedLogins()
		ClearFailedLogins("nonexistent-user")
		// Should not panic
	})

	t.Run("clears lockout state", func(t *testing.T) {
		resetFailedLogins()
		identifier := "locked-user"

		// Lock the account
		for i := 0; i < maxFailedAttempts; i++ {
			RecordFailedLogin(identifier)
		}

		_, _, isLocked := GetLockoutInfo(identifier)
		if !isLocked {
			t.Error("expected account to be locked before clear")
		}

		ClearFailedLogins(identifier)

		_, _, isLocked = GetLockoutInfo(identifier)
		if isLocked {
			t.Error("expected account to not be locked after clear")
		}
	})
}

func TestGetLockoutInfo(t *testing.T) {
	resetFailedLogins()

	t.Run("nonexistent identifier returns zeros", func(t *testing.T) {
		resetFailedLogins()
		attempts, lockedUntil, isLocked := GetLockoutInfo("unknown-user")
		if attempts != 0 {
			t.Errorf("attempts = %d, want 0", attempts)
		}
		if !lockedUntil.IsZero() {
			t.Errorf("lockedUntil = %v, want zero time", lockedUntil)
		}
		if isLocked {
			t.Error("expected isLocked = false")
		}
	})

	t.Run("returns correct attempts below lockout", func(t *testing.T) {
		resetFailedLogins()
		identifier := "partial-user"

		RecordFailedLogin(identifier)
		RecordFailedLogin(identifier)

		attempts, _, isLocked := GetLockoutInfo(identifier)
		if attempts != 2 {
			t.Errorf("attempts = %d, want 2", attempts)
		}
		if isLocked {
			t.Error("expected isLocked = false for attempts below max")
		}
	})

	t.Run("isLocked true only when attempts >= max and within lockout period", func(t *testing.T) {
		resetFailedLogins()
		identifier := "locked-user-test"

		// Record max attempts to trigger lockout
		for i := 0; i < maxFailedAttempts; i++ {
			RecordFailedLogin(identifier)
		}

		attempts, lockedUntil, isLocked := GetLockoutInfo(identifier)
		if attempts != maxFailedAttempts {
			t.Errorf("attempts = %d, want %d", attempts, maxFailedAttempts)
		}
		if !isLocked {
			t.Error("expected isLocked = true")
		}
		if lockedUntil.IsZero() {
			t.Error("expected lockedUntil to be set")
		}
	})
}

func TestResetLockout(t *testing.T) {
	resetFailedLogins()

	t.Run("resets lockout state", func(t *testing.T) {
		resetFailedLogins()
		identifier := "admin-reset-test"

		// Lock the account
		for i := 0; i < maxFailedAttempts; i++ {
			RecordFailedLogin(identifier)
		}

		_, _, isLocked := GetLockoutInfo(identifier)
		if !isLocked {
			t.Error("expected account to be locked before reset")
		}

		ResetLockout(identifier)

		attempts, _, isLocked := GetLockoutInfo(identifier)
		if isLocked {
			t.Error("expected account to not be locked after reset")
		}
		if attempts != 0 {
			t.Errorf("attempts = %d, want 0 after reset", attempts)
		}
	})

	t.Run("resetting nonexistent identifier does not panic", func(t *testing.T) {
		resetFailedLogins()
		ResetLockout("nonexistent-admin-user")
		// Should not panic
	})
}

func TestTrackUserSession(t *testing.T) {
	resetSessionTracking()

	t.Run("tracks new user session", func(t *testing.T) {
		resetSessionTracking()
		TrackUserSession("alice", "session-1")

		username := GetSessionUsername("session-1")
		if username != "alice" {
			t.Errorf("username = %q, want alice", username)
		}
	})

	t.Run("tracks multiple sessions for same user", func(t *testing.T) {
		resetSessionTracking()
		TrackUserSession("bob", "session-a")
		TrackUserSession("bob", "session-b")
		TrackUserSession("bob", "session-c")

		if GetSessionUsername("session-a") != "bob" {
			t.Error("session-a should belong to bob")
		}
		if GetSessionUsername("session-b") != "bob" {
			t.Error("session-b should belong to bob")
		}
		if GetSessionUsername("session-c") != "bob" {
			t.Error("session-c should belong to bob")
		}
	})

	t.Run("tracks sessions for multiple users", func(t *testing.T) {
		resetSessionTracking()
		TrackUserSession("user1", "sess-1")
		TrackUserSession("user2", "sess-2")
		TrackUserSession("user3", "sess-3")

		if GetSessionUsername("sess-1") != "user1" {
			t.Error("sess-1 should belong to user1")
		}
		if GetSessionUsername("sess-2") != "user2" {
			t.Error("sess-2 should belong to user2")
		}
		if GetSessionUsername("sess-3") != "user3" {
			t.Error("sess-3 should belong to user3")
		}
	})
}

func TestGetSessionUsername(t *testing.T) {
	resetSessionTracking()

	t.Run("returns empty for unknown session", func(t *testing.T) {
		resetSessionTracking()
		username := GetSessionUsername("unknown-session")
		if username != "" {
			t.Errorf("username = %q, want empty string", username)
		}
	})

	t.Run("returns correct username for tracked session", func(t *testing.T) {
		resetSessionTracking()
		TrackUserSession("testuser", "test-session-id")

		username := GetSessionUsername("test-session-id")
		if username != "testuser" {
			t.Errorf("username = %q, want testuser", username)
		}
	})

	t.Run("handles multiple users and sessions", func(t *testing.T) {
		resetSessionTracking()
		TrackUserSession("alice", "alice-session-1")
		TrackUserSession("alice", "alice-session-2")
		TrackUserSession("bob", "bob-session-1")

		if GetSessionUsername("alice-session-1") != "alice" {
			t.Error("alice-session-1 should belong to alice")
		}
		if GetSessionUsername("alice-session-2") != "alice" {
			t.Error("alice-session-2 should belong to alice")
		}
		if GetSessionUsername("bob-session-1") != "bob" {
			t.Error("bob-session-1 should belong to bob")
		}
		if GetSessionUsername("unknown") != "" {
			t.Error("unknown session should return empty string")
		}
	})
}

func TestClearCSRFCookie(t *testing.T) {
	t.Run("nil writer does not panic", func(t *testing.T) {
		clearCSRFCookie(nil)
		// Should not panic
	})

	t.Run("sets cookie with maxage -1", func(t *testing.T) {
		w := httptest.NewRecorder()
		clearCSRFCookie(w)

		cookies := w.Result().Cookies()
		if len(cookies) != 1 {
			t.Fatalf("expected 1 cookie, got %d", len(cookies))
		}

		cookie := cookies[0]
		if cookie.Name != "pulse_csrf" {
			t.Errorf("cookie name = %q, want pulse_csrf", cookie.Name)
		}
		if cookie.Value != "" {
			t.Errorf("cookie value = %q, want empty string", cookie.Value)
		}
		if cookie.MaxAge != -1 {
			t.Errorf("cookie MaxAge = %d, want -1", cookie.MaxAge)
		}
	})
}

func TestIssueNewCSRFCookie(t *testing.T) {
	t.Run("nil writer returns empty string", func(t *testing.T) {
		req := httptest.NewRequest("POST", "/api/test", nil)
		token := issueNewCSRFCookie(nil, req, "session-id")
		if token != "" {
			t.Errorf("token = %q, want empty string", token)
		}
	})

	t.Run("nil request returns empty string", func(t *testing.T) {
		w := httptest.NewRecorder()
		token := issueNewCSRFCookie(w, nil, "session-id")
		if token != "" {
			t.Errorf("token = %q, want empty string", token)
		}
	})

	t.Run("empty session ID returns empty string", func(t *testing.T) {
		w := httptest.NewRecorder()
		req := httptest.NewRequest("POST", "/api/test", nil)
		token := issueNewCSRFCookie(w, req, "")
		if token != "" {
			t.Errorf("token = %q, want empty string for empty session", token)
		}
	})

	t.Run("whitespace only session ID returns empty string", func(t *testing.T) {
		w := httptest.NewRecorder()
		req := httptest.NewRequest("POST", "/api/test", nil)
		token := issueNewCSRFCookie(w, req, "   ")
		if token != "" {
			t.Errorf("token = %q, want empty string for whitespace session", token)
		}
	})

	t.Run("valid session returns non-empty token", func(t *testing.T) {
		w := httptest.NewRecorder()
		req := httptest.NewRequest("POST", "/api/test", nil)
		token := issueNewCSRFCookie(w, req, "valid-session-id")
		if token == "" {
			t.Error("expected non-empty token for valid session")
		}

		// Check that a cookie was set
		cookies := w.Result().Cookies()
		found := false
		for _, c := range cookies {
			if c.Name == "pulse_csrf" && c.Value == token {
				found = true
				break
			}
		}
		if !found {
			t.Error("expected pulse_csrf cookie to be set with the token value")
		}
	})
}

func TestFailedLogin_Fields(t *testing.T) {
	fl := FailedLogin{
		Count:       3,
		LastAttempt: fixedTimeForTest(),
		LockedUntil: fixedTimeForTest().Add(15 * 60 * 1000000000),
	}

	if fl.Count != 3 {
		t.Errorf("Count = %d, want 3", fl.Count)
	}
	if fl.LastAttempt.IsZero() {
		t.Error("LastAttempt should not be zero")
	}
	if fl.LockedUntil.IsZero() {
		t.Error("LockedUntil should not be zero")
	}
}

func TestAuditEvent_Fields(t *testing.T) {
	ae := AuditEvent{
		Timestamp: fixedTimeForTest(),
		Event:     "login_attempt",
		User:      "admin",
		IP:        "192.168.1.100",
		Path:      "/api/auth/login",
		Success:   true,
		Details:   "successful login",
	}

	if ae.Event != "login_attempt" {
		t.Errorf("Event = %q, want login_attempt", ae.Event)
	}
	if ae.User != "admin" {
		t.Errorf("User = %q, want admin", ae.User)
	}
	if ae.IP != "192.168.1.100" {
		t.Errorf("IP = %q, want 192.168.1.100", ae.IP)
	}
	if ae.Path != "/api/auth/login" {
		t.Errorf("Path = %q, want /api/auth/login", ae.Path)
	}
	if !ae.Success {
		t.Error("Success should be true")
	}
	if ae.Details != "successful login" {
		t.Errorf("Details = %q, want 'successful login'", ae.Details)
	}
}
