package app

import (
	"database/sql"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"testing"
	"time"
)

func TestExistingDatabaseAddsNewColumns(t *testing.T) {
	path := filepath.Join(t.TempDir(), "legacy.db")
	db, err := sql.Open("sqlite", path)
	if err != nil {
		t.Fatal(err)
	}
	_, err = db.Exec(`CREATE TABLE users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		username TEXT NOT NULL UNIQUE COLLATE NOCASE,
		password_hash TEXT,
		display_name TEXT NOT NULL,
		email TEXT UNIQUE COLLATE NOCASE,
		role TEXT NOT NULL DEFAULT 'member',
		avatar_url TEXT NOT NULL DEFAULT '',
		created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
	);
	INSERT INTO users(username,display_name,role) VALUES('legacy-admin','Legacy Admin','admin');
	CREATE TABLE board_tasks (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		title TEXT NOT NULL,
		description TEXT NOT NULL DEFAULT '',
		status TEXT NOT NULL DEFAULT 'backlog',
		assignee_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
		creator_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
		due_date TEXT,
		created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
	);
	INSERT INTO board_tasks(title,assignee_id,creator_id) VALUES('Legacy card',1,1)`)
	if err != nil {
		t.Fatal(err)
	}
	if err := db.Close(); err != nil {
		t.Fatal(err)
	}

	store, err := OpenStore(path)
	if err != nil {
		t.Fatalf("open legacy database: %v", err)
	}
	defer store.Close()
	var active bool
	if err := store.db.QueryRow(`SELECT active FROM users WHERE username='legacy-admin'`).Scan(&active); err != nil {
		t.Fatalf("read migrated account state: %v", err)
	}
	if !active {
		t.Fatal("legacy account was not active after migration")
	}
	var archivedAt sql.NullString
	if err := store.db.QueryRow(`SELECT archived_at FROM board_tasks WHERE title='Legacy card'`).Scan(&archivedAt); err != nil {
		t.Fatalf("read migrated card archive state: %v", err)
	}
	if archivedAt.Valid {
		t.Fatalf("legacy card was unexpectedly archived: %q", archivedAt.String)
	}
	var importance string
	if err := store.db.QueryRow(`SELECT importance FROM board_tasks WHERE title='Legacy card'`).Scan(&importance); err != nil || importance != "normal" {
		t.Fatalf("legacy card importance: %q, %v", importance, err)
	}
	var migratedAssignees int
	if err := store.db.QueryRow(`SELECT COUNT(*) FROM board_task_assignees WHERE task_id=1 AND user_id=1`).Scan(&migratedAssignees); err != nil || migratedAssignees != 1 {
		t.Fatalf("legacy card assignees: %d, %v", migratedAssignees, err)
	}
}

func TestBoardTasksSortByCreationDate(t *testing.T) {
	store, err := OpenStore(filepath.Join(t.TempDir(), "sort.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer store.Close()
	olderID, err := store.CreateBoardTask(1, "Created later", "", "backlog", "normal", nil, "")
	if err != nil {
		t.Fatal(err)
	}
	newerID, err := store.CreateBoardTask(1, "Created earlier", "", "backlog", "normal", nil, "")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := store.db.Exec(`UPDATE board_tasks SET created_at=CASE id WHEN ? THEN ? ELSE ? END`,
		olderID, "2026-07-29 12:00:00", "2026-07-29 11:00:00"); err != nil {
		t.Fatal(err)
	}
	tasks, err := store.BoardTasks()
	if err != nil {
		t.Fatal(err)
	}
	if len(tasks) != 2 || tasks[0].ID != olderID || tasks[1].ID != newerID {
		t.Fatalf("cards were not sorted newest-created-first: %#v", tasks)
	}
}

func TestLoginAttendanceAndReportFlow(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "test.db"),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	login := formRequest(t, s.http.Handler, "/login", url.Values{
		"username": {"admin"},
		"password": {"admin123"},
	}, nil)
	if login.Code != http.StatusSeeOther {
		t.Fatalf("login status: got %d, body %s", login.Code, login.Body.String())
	}
	cookies := login.Result().Cookies()
	if len(cookies) == 0 {
		t.Fatal("login did not set a session cookie")
	}
	session := cookies[0]

	dashboardReq := httptest.NewRequest(http.MethodGet, "/", nil)
	dashboardReq.AddCookie(session)
	dashboard := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(dashboard, dashboardReq)
	if dashboard.Code != http.StatusOK || !strings.Contains(dashboard.Body.String(), "PERSIAN CALENDAR") {
		t.Fatalf("dashboard status/body: %d %s", dashboard.Code, dashboard.Body.String())
	}
	if !strings.Contains(dashboard.Body.String(), "/day?date=") {
		t.Fatal("dashboard calendar days do not link to the team day view")
	}
	csrf := extractCSRF(t, dashboard.Body.String())

	createUser := formRequest(t, s.http.Handler, "/admin/users", url.Values{
		"csrf":         {csrf},
		"username":     {"navid"},
		"password":     {"temporary-password"},
		"display_name": {"Navid Somethingi"},
		"email":        {"navid@example.test"},
		"role":         {"member"},
	}, session)
	if createUser.Code != http.StatusSeeOther {
		t.Fatalf("create user status: got %d, body %s", createUser.Code, createUser.Body.String())
	}
	if _, err := s.store.Authenticate("navid", "temporary-password"); err != nil {
		t.Fatalf("created teammate could not authenticate: %v", err)
	}
	users, err := s.store.Users()
	if err != nil {
		t.Fatal(err)
	}
	var testuser User
	for _, user := range users {
		if user.Username == "navid" {
			testuser = user
			break
		}
	}
	if testuser.ID == 0 || !testuser.Active {
		t.Fatalf("created teammate missing or inactive: %#v", testuser)
	}
	testUserToken, _, err := s.store.CreateSession(testuser.ID)
	if err != nil {
		t.Fatal(err)
	}
	lockUser := formRequest(t, s.http.Handler, "/admin/users/"+strconv.FormatInt(testuser.ID, 10)+"/status", url.Values{
		"csrf":   {csrf},
		"action": {"lock"},
	}, session)
	if lockUser.Code != http.StatusSeeOther || lockUser.Header().Get("Location") != "/admin/users?flash=Account+locked" {
		t.Fatalf("lock user: got %d location %q", lockUser.Code, lockUser.Header().Get("Location"))
	}
	if _, err := s.store.Authenticate("navid", "temporary-password"); err == nil || !strings.Contains(err.Error(), "locked") {
		t.Fatalf("locked teammate authentication result: %v", err)
	}
	if _, _, err := s.store.Session(testUserToken); err == nil {
		t.Fatal("locking a teammate did not revoke the active session")
	}
	if activeUsers, err := s.store.ActiveUsers(); err != nil || len(activeUsers) != 1 {
		t.Fatalf("locked teammate was not hidden from active users: %#v, %v", activeUsers, err)
	}
	if roster, err := s.store.DayRoster(time.Now().Format("2006-01-02")); err != nil || len(roster) != 1 {
		t.Fatalf("locked teammate was not hidden from the team roster: %#v, %v", roster, err)
	}
	usersRequest := httptest.NewRequest(http.MethodGet, "/admin/users", nil)
	usersRequest.AddCookie(session)
	usersResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(usersResponse, usersRequest)
	if usersResponse.Code != http.StatusOK || !strings.Contains(usersResponse.Body.String(), "Locked") || !strings.Contains(usersResponse.Body.String(), "Unlock") {
		t.Fatalf("locked account state missing from admin directory: %d %s", usersResponse.Code, usersResponse.Body.String())
	}
	lockSelf := formRequest(t, s.http.Handler, "/admin/users/1/status", url.Values{
		"csrf":   {csrf},
		"action": {"lock"},
	}, session)
	if lockSelf.Code != http.StatusSeeOther || !strings.Contains(lockSelf.Header().Get("Location"), "error=") {
		t.Fatalf("administrator could lock their own account: %d %q", lockSelf.Code, lockSelf.Header().Get("Location"))
	}
	unlockUser := formRequest(t, s.http.Handler, "/admin/users/"+strconv.FormatInt(testuser.ID, 10)+"/status", url.Values{
		"csrf":   {csrf},
		"action": {"unlock"},
	}, session)
	if unlockUser.Code != http.StatusSeeOther || unlockUser.Header().Get("Location") != "/admin/users?flash=Account+unlocked" {
		t.Fatalf("unlock user: got %d location %q", unlockUser.Code, unlockUser.Header().Get("Location"))
	}
	if _, err := s.store.Authenticate("navid", "temporary-password"); err != nil {
		t.Fatalf("unlocked teammate could not authenticate: %v", err)
	}
	newPassword := formRequest(t, s.http.Handler, "/admin/users/"+strconv.FormatInt(testuser.ID, 10)+"/password", url.Values{
		"csrf":             {csrf},
		"password":         {"changed-password"},
		"password_confirm": {"changed-password"},
	}, session)
	if newPassword.Code != http.StatusSeeOther || newPassword.Header().Get("Location") != "/admin/users?flash=Password+updated" {
		t.Fatalf("change password: got %d location %q", newPassword.Code, newPassword.Header().Get("Location"))
	}
	if _, err := s.store.Authenticate("navid", "temporary-password"); err == nil {
		t.Fatal("old password still authenticates after reset")
	}
	if _, err := s.store.Authenticate("navid", "changed-password"); err != nil {
		t.Fatalf("new password does not authenticate: %v", err)
	}
	deleteUser := formRequest(t, s.http.Handler, "/admin/users/"+strconv.FormatInt(testuser.ID, 10)+"/delete", url.Values{"csrf": {csrf}}, session)
	if deleteUser.Code != http.StatusSeeOther || deleteUser.Header().Get("Location") != "/admin/users?flash=Account+deleted" {
		t.Fatalf("delete user: got %d location %q", deleteUser.Code, deleteUser.Header().Get("Location"))
	}
	if _, err := s.store.Authenticate("navid", "changed-password"); err == nil {
		t.Fatal("deleted account still authenticates")
	}

	checkIn := formRequest(t, s.http.Handler, "/attendance/check-in", url.Values{
		"csrf": {csrf},
		"mode": {"office"},
	}, session)
	if checkIn.Code != http.StatusSeeOther {
		t.Fatalf("check-in status: got %d, body %s", checkIn.Code, checkIn.Body.String())
	}

	reportReq := httptest.NewRequest(http.MethodGet, "/reports/attendance.csv?start=2020-01-01&end=2030-01-01", nil)
	reportReq.AddCookie(session)
	report := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(report, reportReq)
	if report.Code != http.StatusOK {
		t.Fatalf("report status: got %d, body %s", report.Code, report.Body.String())
	}
	if got := report.Header().Get("Content-Type"); !strings.Contains(got, "text/csv") {
		t.Fatalf("report content type: %s", got)
	}
	if !strings.Contains(report.Body.String(), "Workspace Admin") {
		t.Fatalf("report did not contain attendance row: %s", report.Body.String())
	}
}

func TestAttendanceTimesUseConfiguredTimezoneAndShowCheckout(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "timezone.db"),
		Timezone:     "Asia/Tehran",
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	day := time.Now().Format("2006-01-02")
	legacyCheckIn := day + " 11:48:51.004976173 +0330 +0330 m=+18.184759346"
	checkOut := day + " 11:49:32.496818296 +0330 +0330 m=+59.676601465"
	if _, err := s.store.db.Exec(
		`INSERT INTO attendance(user_id,day,check_in,check_out,mode) VALUES(?,?,?,?,?)`,
		1, day, legacyCheckIn, checkOut, "office",
	); err != nil {
		t.Fatal(err)
	}
	token, _, err := s.store.CreateSession(1)
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.AddCookie(&http.Cookie{Name: "teammate_session", Value: token})
	response := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("dashboard status: %d body %s", response.Code, response.Body.String())
	}
	for _, expected := range []string{"11:48", "11:49", "Day complete"} {
		if !strings.Contains(response.Body.String(), expected) {
			t.Fatalf("dashboard does not include %q: %s", expected, response.Body.String())
		}
	}
	if strings.Contains(response.Body.String(), `action="/attendance/check-out"`) {
		t.Fatal("completed attendance still shows the checkout action")
	}
}

func TestPersianRequestDateParsing(t *testing.T) {
	got, ok := parseUserDate("1405-05-06")
	if !ok || got != "2026-07-28" {
		t.Fatalf("got %q, %v; want 2026-07-28, true", got, ok)
	}
	if _, ok := parseUserDate("1400-12-30"); ok {
		t.Fatal("accepted an invalid non-leap Esfand date")
	}
}

func TestReviewedRequestCanBeListed(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "review.db"),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	if err := s.store.CreateRequest(1, "leave", "2026-07-28", "2026-07-29", "Personal"); err != nil {
		t.Fatal(err)
	}
	pending, err := s.store.Requests(1, true, "pending")
	if err != nil || len(pending) != 1 {
		t.Fatalf("pending requests: %#v, %v", pending, err)
	}
	if err := s.store.ReviewRequest(t.Context(), pending[0].ID, 1, "approved", "Approved"); err != nil {
		t.Fatal(err)
	}
	reviewed, err := s.store.Requests(1, true, "")
	if err != nil {
		t.Fatalf("listing reviewed request failed: %v", err)
	}
	if len(reviewed) != 1 || reviewed[0].Status != "approved" {
		t.Fatalf("unexpected reviewed requests: %#v", reviewed)
	}

	token, _, err := s.store.CreateSession(1)
	if err != nil {
		t.Fatal(err)
	}
	adminRequest := httptest.NewRequest(http.MethodGet, "/admin/requests?flash=Request+reviewed", nil)
	adminRequest.AddCookie(&http.Cookie{Name: "teammate_session", Value: token})
	adminResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(adminResponse, adminRequest)
	if adminResponse.Code != http.StatusOK || !strings.Contains(adminResponse.Body.String(), "Request reviewed") {
		t.Fatalf("review redirect page: got %d, body %s", adminResponse.Code, adminResponse.Body.String())
	}
}

func TestPublicRegistrationAndIdentifierLogin(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "registration.db"),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	healthRequest := httptest.NewRequest(http.MethodGet, "/healthz", nil)
	healthResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(healthResponse, healthRequest)
	if healthResponse.Code != http.StatusOK || healthResponse.Body.String() != `{"status":"ok"}` {
		t.Fatalf("health endpoint: got %d, body %s", healthResponse.Code, healthResponse.Body.String())
	}

	registerPage := httptest.NewRequest(http.MethodGet, "/register", nil)
	registerView := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(registerView, registerPage)
	if registerView.Code != http.StatusOK || !strings.Contains(registerView.Body.String(), "Create your account") {
		t.Fatalf("register page: got %d, body %s", registerView.Code, registerView.Body.String())
	}
	for _, expected := range []string{"Vazirmatn-font-face.css", "/static/theme.js", "data-theme-toggle"} {
		if !strings.Contains(registerView.Body.String(), expected) {
			t.Fatalf("register page does not include %q", expected)
		}
	}
	if !strings.Contains(registerView.Body.String(), `/static/favicon.svg`) {
		t.Fatal("register page does not include the favicon")
	}

	themeRequest := httptest.NewRequest(http.MethodGet, "/static/theme.js", nil)
	themeResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(themeResponse, themeRequest)
	if themeResponse.Code != http.StatusOK || !strings.Contains(themeResponse.Body.String(), "hamkar-theme") {
		t.Fatalf("theme asset: got %d, body %s", themeResponse.Code, themeResponse.Body.String())
	}

	faviconRequest := httptest.NewRequest(http.MethodGet, "/static/favicon.svg", nil)
	faviconResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(faviconResponse, faviconRequest)
	if faviconResponse.Code != http.StatusOK || !strings.Contains(faviconResponse.Body.String(), `<svg`) {
		t.Fatalf("favicon asset: got %d, body %s", faviconResponse.Code, faviconResponse.Body.String())
	}

	register := formRequest(t, s.http.Handler, "/register", url.Values{
		"display_name":     {"Neda Karimi"},
		"email":            {"neda@example.test"},
		"password":         {"secure-password"},
		"password_confirm": {"secure-password"},
	}, nil)
	if register.Code != http.StatusSeeOther || register.Header().Get("Location") != "/?flash=Welcome+to+Hamkar" {
		t.Fatalf("registration: got %d location %q body %s", register.Code, register.Header().Get("Location"), register.Body.String())
	}
	if len(register.Result().Cookies()) == 0 {
		t.Fatal("registration did not sign the new member in")
	}

	user, err := s.store.Authenticate("neda@example.test", "secure-password")
	if err != nil {
		t.Fatalf("email login failed: %v", err)
	}
	if user.Username != "neda" || user.Role != "member" {
		t.Fatalf("unexpected registered user: %#v", user)
	}
	if _, err := s.store.Authenticate("neda", "secure-password"); err != nil {
		t.Fatalf("generated username login failed: %v", err)
	}
}

func TestRequestPageIncludesJalaliPicker(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "picker.db"),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	token, _, err := s.store.CreateSession(1)
	if err != nil {
		t.Fatal(err)
	}
	pageRequest := httptest.NewRequest(http.MethodGet, "/requests", nil)
	pageRequest.AddCookie(&http.Cookie{Name: "teammate_session", Value: token})
	pageResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(pageResponse, pageRequest)
	if pageResponse.Code != http.StatusOK {
		t.Fatalf("request page status: %d", pageResponse.Code)
	}
	for _, expected := range []string{"/static/jalali-picker.js", "data-jalali-picker", "jalali-trigger"} {
		if !strings.Contains(pageResponse.Body.String(), expected) {
			t.Fatalf("request page does not include %q", expected)
		}
	}

	assetRequest := httptest.NewRequest(http.MethodGet, "/static/jalali-picker.js", nil)
	assetResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(assetResponse, assetRequest)
	if assetResponse.Code != http.StatusOK || !strings.Contains(assetResponse.Body.String(), "gregorianToJalali") {
		t.Fatalf("picker asset: got %d, body %s", assetResponse.Code, assetResponse.Body.String())
	}
}

func TestTeamDayShowsPresentRemoteAndAbsent(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "day.db"),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	remoteID, err := s.store.CreateUser("remote-user", "secure-password", "Remote Teammate", "remote@example.test", "member")
	if err != nil {
		t.Fatal(err)
	}
	leaveID, err := s.store.CreateUser("leave-user", "secure-password", "Absent Teammate", "leave@example.test", "member")
	if err != nil {
		t.Fatal(err)
	}
	const selectedDay = "2030-01-02"
	if err := s.store.CheckIn(1, selectedDay, "office"); err != nil {
		t.Fatal(err)
	}
	if err := s.store.CreateRequest(remoteID, "remote", selectedDay, selectedDay, "Working from home"); err != nil {
		t.Fatal(err)
	}
	if err := s.store.CreateRequest(leaveID, "leave", selectedDay, selectedDay, "Holiday"); err != nil {
		t.Fatal(err)
	}
	for _, userID := range []int64{remoteID, leaveID} {
		requests, err := s.store.Requests(userID, false, "pending")
		if err != nil || len(requests) != 1 {
			t.Fatalf("pending request for %d: %#v, %v", userID, requests, err)
		}
		if err := s.store.ReviewRequest(t.Context(), requests[0].ID, 1, "approved", "Approved"); err != nil {
			t.Fatal(err)
		}
	}

	token, _, err := s.store.CreateSession(1)
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodGet, "/day?date="+selectedDay+"&view=day", nil)
	request.AddCookie(&http.Cookie{Name: "teammate_session", Value: token})
	response := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("day page status: %d, body %s", response.Code, response.Body.String())
	}
	for _, expected := range []string{
		"Workspace Admin", "Remote Teammate", "Absent Teammate",
		"Office · checked in", "Approved remote day", "Approved time off",
		`class="presence-badge present"`, `class="presence-badge remote"`, `class="presence-badge absent"`,
	} {
		if !strings.Contains(response.Body.String(), expected) {
			t.Fatalf("day page does not include %q", expected)
		}
	}

	weekRequest := httptest.NewRequest(http.MethodGet, "/day?date="+selectedDay+"&view=week", nil)
	weekRequest.AddCookie(&http.Cookie{Name: "teammate_session", Value: token})
	weekResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(weekResponse, weekRequest)
	if weekResponse.Code != http.StatusOK {
		t.Fatalf("week page status: %d, body %s", weekResponse.Code, weekResponse.Body.String())
	}
	body := weekResponse.Body.String()
	if !strings.Contains(body, "TEAM WEEK") || !strings.Contains(body, "SATURDAY — FRIDAY") {
		t.Fatalf("week page header is missing: %s", body)
	}
	if count := strings.Count(body, `class="week-day-column`); count != 7 {
		t.Fatalf("week page has %d day columns, want 7", count)
	}
	saturday := strings.Index(body, ">Saturday<")
	friday := strings.Index(body, ">Friday<")
	if saturday < 0 || friday < 0 || saturday >= friday {
		t.Fatal("week page is not ordered Saturday through Friday")
	}
	for _, expected := range []string{"Workspace Admin", "Remote Teammate", "Absent Teammate", "Approved remote day", "Approved time off"} {
		if !strings.Contains(body, expected) {
			t.Fatalf("week page does not include %q", expected)
		}
	}

	defaultRequest := httptest.NewRequest(http.MethodGet, "/day?date="+selectedDay, nil)
	defaultRequest.AddCookie(&http.Cookie{Name: "teammate_session", Value: token})
	defaultResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(defaultResponse, defaultRequest)
	if defaultResponse.Code != http.StatusOK || !strings.Contains(defaultResponse.Body.String(), "TEAM WEEK") {
		t.Fatalf("default team view is not weekly: %d %s", defaultResponse.Code, defaultResponse.Body.String())
	}
}

func TestWorkUpdateSubmissionFeedAndCSV(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "updates.db"),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	login := formRequest(t, s.http.Handler, "/login", url.Values{
		"identifier": {"admin"},
		"password":   {"admin123"},
	}, nil)
	session := login.Result().Cookies()[0]
	dashboardRequest := httptest.NewRequest(http.MethodGet, "/", nil)
	dashboardRequest.AddCookie(session)
	dashboardResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(dashboardResponse, dashboardRequest)
	csrf := extractCSRF(t, dashboardResponse.Body.String())

	submission := formRequest(t, s.http.Handler, "/updates", url.Values{
		"csrf":         {csrf},
		"period_start": {"1405-05-06"},
		"period_end":   {"1405-05-09"},
		"status":       {"blocked"},
		"note":         {"Completed task ABC.\nBlocked on staging access; task XYZ is pending."},
	}, session)
	if submission.Code != http.StatusSeeOther || submission.Header().Get("Location") != "/updates?flash=Work+update+shared" {
		t.Fatalf("work update submission: got %d location %q body %s", submission.Code, submission.Header().Get("Location"), submission.Body.String())
	}
	if err := s.store.CheckIn(1, "2026-07-28", "office"); err != nil {
		t.Fatal(err)
	}
	if err := s.store.CheckIn(1, "2026-07-29", "remote"); err != nil {
		t.Fatal(err)
	}
	if err := s.store.CreateRequest(1, "leave", "2026-07-30", "2026-07-31", "Time off"); err != nil {
		t.Fatal(err)
	}
	leaveRequests, err := s.store.Requests(1, false, "pending")
	if err != nil || len(leaveRequests) != 1 {
		t.Fatalf("leave request: %#v, %v", leaveRequests, err)
	}
	if err := s.store.ReviewRequest(t.Context(), leaveRequests[0].ID, 1, "approved", "Approved"); err != nil {
		t.Fatal(err)
	}

	feedRequest := httptest.NewRequest(http.MethodGet, "/updates", nil)
	feedRequest.AddCookie(session)
	feedResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(feedResponse, feedRequest)
	if feedResponse.Code != http.StatusOK {
		t.Fatalf("work update feed status: %d", feedResponse.Code)
	}
	for _, expected := range []string{"Completed task ABC.", "Blocked on staging access", "1405-05-06", "1405-05-09", "Blocked"} {
		if !strings.Contains(feedResponse.Body.String(), expected) {
			t.Fatalf("work update feed does not include %q", expected)
		}
	}

	exportRequest := httptest.NewRequest(http.MethodGet, "/reports/work-updates.csv?start=2026-07-01&end=2026-08-01", nil)
	exportRequest.AddCookie(session)
	exportResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(exportResponse, exportRequest)
	if exportResponse.Code != http.StatusOK || !strings.Contains(exportResponse.Body.String(), "Completed task ABC.") {
		t.Fatalf("work update CSV: got %d body %s", exportResponse.Code, exportResponse.Body.String())
	}
	if !strings.Contains(exportResponse.Header().Get("Content-Disposition"), "work-updates-") {
		t.Fatalf("unexpected work update CSV filename: %s", exportResponse.Header().Get("Content-Disposition"))
	}

	reportRequest := httptest.NewRequest(http.MethodGet, "/reports?start=2026-07-01&end=2026-08-01", nil)
	reportRequest.AddCookie(session)
	reportResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(reportResponse, reportRequest)
	if reportResponse.Code != http.StatusOK || !strings.Contains(reportResponse.Body.String(), "ACCUMULATED RESULT") {
		t.Fatalf("accumulated report page: got %d body %s", reportResponse.Code, reportResponse.Body.String())
	}

	summaryRequest := httptest.NewRequest(http.MethodGet, "/reports/summary.csv?start=2026-07-01&end=2026-08-01", nil)
	summaryRequest.AddCookie(session)
	summaryResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(summaryResponse, summaryRequest)
	if summaryResponse.Code != http.StatusOK {
		t.Fatalf("summary CSV status: %d body %s", summaryResponse.Code, summaryResponse.Body.String())
	}
	if !strings.Contains(summaryResponse.Body.String(), "Workspace Admin,admin,1,1,2,0,1,0,1") {
		t.Fatalf("summary CSV did not contain accumulated values: %s", summaryResponse.Body.String())
	}
}

func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "board.db"),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	memberID, err := s.store.CreateUser("board-member", "secure-password", "Board Member", "board@example.test", "member")
	if err != nil {
		t.Fatal(err)
	}
	adminToken, adminCSRF, err := s.store.CreateSession(1)
	if err != nil {
		t.Fatal(err)
	}
	adminCookie := &http.Cookie{Name: "teammate_session", Value: adminToken}
	for _, tag := range []struct {
		name  string
		color string
	}{
		{"Backend", "green"},
		{"Urgent", "red"},
	} {
		response := formRequest(t, s.http.Handler, "/board/tags", url.Values{
			"csrf":  {adminCSRF},
			"name":  {tag.name},
			"color": {tag.color},
		}, adminCookie)
		if response.Code != http.StatusSeeOther {
			t.Fatalf("create board tag %s: %d %s", tag.name, response.Code, response.Body.String())
		}
	}
	tags, err := s.store.BoardTags()
	if err != nil || len(tags) != 2 {
		t.Fatalf("board tags after create: %#v, %v", tags, err)
	}
	reusedTag, err := s.store.FindOrCreateBoardTag("backend", "red")
	if err != nil || reusedTag.ID != tags[0].ID || reusedTag.Color != "green" {
		t.Fatalf("existing inline tag was not reused: %#v, %v", reusedTag, err)
	}
	create := formRequest(t, s.http.Handler, "/board/tasks", url.Values{
		"csrf":        {adminCSRF},
		"title":       {"Ship the onboarding flow"},
		"description": {"Finish QA and publish the release."},
		"status":      {"backlog"},
		"assignee_ids": {
			"1",
			strconv.FormatInt(memberID, 10),
		},
		"importance": {"high"},
		"due_date":   {"1405-05-06"},
		"tag_ids": {
			strconv.FormatInt(tags[0].ID, 10),
			strconv.FormatInt(tags[1].ID, 10),
		},
		"new_tag_name":  {"Frontend"},
		"new_tag_color": {"blue"},
	}, adminCookie)
	if create.Code != http.StatusSeeOther || create.Header().Get("Location") != "/board?flash=Task+added+to+the+team+board" {
		t.Fatalf("create board task: got %d location %q body %s", create.Code, create.Header().Get("Location"), create.Body.String())
	}
	tasks, err := s.store.BoardTasks()
	if err != nil || len(tasks) != 1 {
		t.Fatalf("board tasks after create: %#v, %v", tasks, err)
	}
	if len(tasks[0].Assignees) != 2 || tasks[0].Importance != "high" || tasks[0].DueDate != "2026-07-28" {
		t.Fatalf("unexpected assigned board task: %#v", tasks[0])
	}
	if len(tasks[0].Tags) != 3 {
		t.Fatalf("task tags after create: %#v", tasks[0].Tags)
	}

	memberToken, memberCSRF, err := s.store.CreateSession(memberID)
	if err != nil {
		t.Fatal(err)
	}
	memberCookie := &http.Cookie{Name: "teammate_session", Value: memberToken}
	boardRequest := httptest.NewRequest(http.MethodGet, "/board", nil)
	boardRequest.AddCookie(memberCookie)
	boardResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(boardResponse, boardRequest)
	if boardResponse.Code != http.StatusOK {
		t.Fatalf("board page status: %d body %s", boardResponse.Code, boardResponse.Body.String())
	}
	for _, expected := range []string{"Ship the onboarding flow", "Board Member", "Workspace Admin", "1405-05-06", "High priority", "Backlog", "To do", "Filters", "Manage tags", "Tags", "select multiple", "Create a new tag", "People &amp; importance", "To-do list", "Backend", "Frontend", "Urgent", "/static/board.js", `data-board-column="backlog"`} {
		if !strings.Contains(boardResponse.Body.String(), expected) {
			t.Fatalf("board page does not include %q", expected)
		}
	}
	adminBoardRequest := httptest.NewRequest(http.MethodGet, "/board", nil)
	adminBoardRequest.AddCookie(adminCookie)
	adminBoardResponse := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(adminBoardResponse, adminBoardRequest)
	for _, expected := range []string{"Archive", "Delete", "Delete permanently"} {
		if !strings.Contains(adminBoardResponse.Body.String(), expected) {
			t.Fatalf("card owner controls do not include %q", expected)
		}
	}

	tagPath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/tags"
	setTags := formRequest(t, s.http.Handler, tagPath, url.Values{
		"csrf":    {memberCSRF},
		"tag_ids": {strconv.FormatInt(tags[0].ID, 10)},
	}, memberCookie)
	if setTags.Code != http.StatusSeeOther {
		t.Fatalf("set task tags status: %d", setTags.Code)
	}
	tasks, _ = s.store.BoardTasks()
	if len(tasks[0].Tags) != 1 || tasks[0].Tags[0].ID != tags[0].ID {
		t.Fatalf("task tags were not updated: %#v", tasks[0].Tags)
	}
	detailsPath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/details"
	setDetails := formRequest(t, s.http.Handler, detailsPath, url.Values{
		"csrf":         {memberCSRF},
		"assignee_ids": {strconv.FormatInt(memberID, 10)},
		"importance":   {"urgent"},
	}, memberCookie)
	if setDetails.Code != http.StatusSeeOther || setDetails.Header().Get("Location") != "/board?flash=Card+details+updated" {
		t.Fatalf("set task details: got %d location %q", setDetails.Code, setDetails.Header().Get("Location"))
	}
	tasks, _ = s.store.BoardTasks()
	if len(tasks[0].Assignees) != 1 || tasks[0].Assignees[0].ID != memberID || tasks[0].Importance != "urgent" {
		t.Fatalf("task people or importance were not updated: %#v", tasks[0])
	}
	todoPath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/todos"
	createTodo := formRequest(t, s.http.Handler, todoPath, url.Values{
		"csrf": {memberCSRF},
		"body": {"Confirm the onboarding handoff"},
	}, memberCookie)
	if createTodo.Code != http.StatusSeeOther || createTodo.Header().Get("Location") != "/board?flash=To-do+item+added" {
		t.Fatalf("create to-do: got %d location %q", createTodo.Code, createTodo.Header().Get("Location"))
	}
	tasks, _ = s.store.BoardTasks()
	if len(tasks[0].Todos) != 1 || tasks[0].Todos[0].Body != "Confirm the onboarding handoff" || tasks[0].TodoDone != 0 {
		t.Fatalf("created to-do was not loaded: %#v", tasks[0])
	}
	toggleTodoPath := todoPath + "/" + strconv.FormatInt(tasks[0].Todos[0].ID, 10) + "/toggle"
	toggleTodo := formRequest(t, s.http.Handler, toggleTodoPath, url.Values{
		"csrf":      {memberCSRF},
		"completed": {"on"},
	}, memberCookie)
	if toggleTodo.Code != http.StatusSeeOther || toggleTodo.Header().Get("Location") != "/board?flash=To-do+item+updated" {
		t.Fatalf("toggle to-do: got %d location %q", toggleTodo.Code, toggleTodo.Header().Get("Location"))
	}
	tasks, _ = s.store.BoardTasks()
	if !tasks[0].Todos[0].Completed || tasks[0].TodoDone != 1 {
		t.Fatalf("to-do completion was not saved: %#v", tasks[0].Todos)
	}
	deleteTodoPath := todoPath + "/" + strconv.FormatInt(tasks[0].Todos[0].ID, 10) + "/delete"
	deleteTodo := formRequest(t, s.http.Handler, deleteTodoPath, url.Values{"csrf": {memberCSRF}}, memberCookie)
	if deleteTodo.Code != http.StatusSeeOther || deleteTodo.Header().Get("Location") != "/board?flash=To-do+item+removed" {
		t.Fatalf("delete to-do: got %d location %q", deleteTodo.Code, deleteTodo.Header().Get("Location"))
	}
	tasks, _ = s.store.BoardTasks()
	if len(tasks[0].Todos) != 0 {
		t.Fatalf("to-do was not deleted: %#v", tasks[0].Todos)
	}
	matchingFilterPath := "/board?tag=" + strconv.FormatInt(tags[0].ID, 10) +
		"&assignee=" + strconv.FormatInt(memberID, 10) + "&priority=urgent"
	matchingFilterRequest := httptest.NewRequest(http.MethodGet, matchingFilterPath, nil)
	matchingFilterRequest.AddCookie(memberCookie)
	matchingFilter := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(matchingFilter, matchingFilterRequest)
	if matchingFilter.Code != http.StatusOK || !strings.Contains(matchingFilter.Body.String(), "Ship the onboarding flow") || !strings.Contains(matchingFilter.Body.String(), "Filters (3)") || !strings.Contains(matchingFilter.Body.String(), "Clear all") {
		t.Fatalf("matching board filter: %d %s", matchingFilter.Code, matchingFilter.Body.String())
	}
	for _, path := range []string{
		"/board?priority=high",
		"/board?assignee=1",
		"/board?tag=999999",
	} {
		request := httptest.NewRequest(http.MethodGet, path, nil)
		request.AddCookie(memberCookie)
		response := httptest.NewRecorder()
		s.http.Handler.ServeHTTP(response, request)
		if response.Code != http.StatusOK || strings.Contains(response.Body.String(), "Ship the onboarding flow") || !strings.Contains(response.Body.String(), "No matching cards") {
			t.Fatalf("non-matching board filter %q: %d %s", path, response.Code, response.Body.String())
		}
	}

	deleteTagPath := "/board/tags/" + strconv.FormatInt(tags[1].ID, 10) + "/delete"
	deleteTag := formRequest(t, s.http.Handler, deleteTagPath, url.Values{"csrf": {adminCSRF}}, adminCookie)
	if deleteTag.Code != http.StatusSeeOther {
		t.Fatalf("delete tag status: %d", deleteTag.Code)
	}
	if remainingTags, _ := s.store.BoardTags(); len(remainingTags) != 2 {
		t.Fatalf("tag manager did not delete tag: %#v", remainingTags)
	}

	movePath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/move"
	move := formRequest(t, s.http.Handler, movePath, url.Values{
		"csrf":   {memberCSRF},
		"status": {"in_progress"},
	}, memberCookie)
	if move.Code != http.StatusSeeOther {
		t.Fatalf("move board task status: %d", move.Code)
	}
	tasks, _ = s.store.BoardTasks()
	if tasks[0].Status != "in_progress" {
		t.Fatalf("task did not move: %#v", tasks[0])
	}

	deletePath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/delete"
	deniedDelete := formRequest(t, s.http.Handler, deletePath, url.Values{"csrf": {memberCSRF}}, memberCookie)
	if deniedDelete.Code != http.StatusSeeOther || !strings.Contains(deniedDelete.Header().Get("Location"), "error=") {
		t.Fatalf("non-creator delete was not rejected: %d %q", deniedDelete.Code, deniedDelete.Header().Get("Location"))
	}
	if remaining, _ := s.store.BoardTasks(); len(remaining) != 1 {
		t.Fatal("non-creator removed a board task")
	}

	archivePath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/archive"
	deniedArchive := formRequest(t, s.http.Handler, archivePath, url.Values{"csrf": {memberCSRF}}, memberCookie)
	if deniedArchive.Code != http.StatusSeeOther || !strings.Contains(deniedArchive.Header().Get("Location"), "error=") {
		t.Fatalf("non-creator archive was not rejected: %d %q", deniedArchive.Code, deniedArchive.Header().Get("Location"))
	}
	adminArchive := formRequest(t, s.http.Handler, archivePath, url.Values{"csrf": {adminCSRF}}, adminCookie)
	if adminArchive.Code != http.StatusSeeOther || adminArchive.Header().Get("Location") != "/board?flash=Task+archived" {
		t.Fatalf("admin archive: got %d location %q", adminArchive.Code, adminArchive.Header().Get("Location"))
	}
	if active, _ := s.store.BoardTasks(); len(active) != 0 {
		t.Fatalf("archived card remained on active board: %#v", active)
	}
	archived, err := s.store.ArchivedBoardTasks()
	if err != nil || len(archived) != 1 || archived[0].Title != "Ship the onboarding flow" {
		t.Fatalf("archived cards: %#v, %v", archived, err)
	}
	archivedPageRequest := httptest.NewRequest(http.MethodGet, "/board", nil)
	archivedPageRequest.AddCookie(adminCookie)
	archivedPage := httptest.NewRecorder()
	s.http.Handler.ServeHTTP(archivedPage, archivedPageRequest)
	for _, expected := range []string{"Archived cards", "Ship the onboarding flow", "Restore", "Delete permanently"} {
		if !strings.Contains(archivedPage.Body.String(), expected) {
			t.Fatalf("archived card panel does not include %q", expected)
		}
	}
	restorePath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/restore"
	adminRestore := formRequest(t, s.http.Handler, restorePath, url.Values{"csrf": {adminCSRF}}, adminCookie)
	if adminRestore.Code != http.StatusSeeOther || adminRestore.Header().Get("Location") != "/board?flash=Task+restored" {
		t.Fatalf("admin restore: got %d location %q", adminRestore.Code, adminRestore.Header().Get("Location"))
	}
	if active, _ := s.store.BoardTasks(); len(active) != 1 {
		t.Fatalf("restored card did not return to board: %#v", active)
	}
	adminArchive = formRequest(t, s.http.Handler, archivePath, url.Values{"csrf": {adminCSRF}}, adminCookie)
	if adminArchive.Code != http.StatusSeeOther {
		t.Fatalf("second admin archive status: %d", adminArchive.Code)
	}
	adminDelete := formRequest(t, s.http.Handler, deletePath, url.Values{"csrf": {adminCSRF}}, adminCookie)
	if adminDelete.Code != http.StatusSeeOther || adminDelete.Header().Get("Location") != "/board?flash=Task+permanently+deleted" {
		t.Fatalf("admin delete: got %d location %q", adminDelete.Code, adminDelete.Header().Get("Location"))
	}
	if remaining, _ := s.store.BoardTasks(); len(remaining) != 0 {
		t.Fatal("admin could not remove the board task")
	}
	if remaining, _ := s.store.ArchivedBoardTasks(); len(remaining) != 0 {
		t.Fatal("permanently deleted card remained in archive")
	}
}

func formRequest(t *testing.T, handler http.Handler, path string, values url.Values, cookie *http.Cookie) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(values.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	if cookie != nil {
		req.AddCookie(cookie)
	}
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)
	return rec
}

func extractCSRF(t *testing.T, body string) string {
	t.Helper()
	re := regexp.MustCompile(`name="csrf" value="([^"]+)"`)
	match := re.FindStringSubmatch(body)
	if len(match) != 2 {
		t.Fatal("page did not contain a CSRF token")
	}
	return match[1]
}

func TestStaticHandlerSetsFontContentTypes(t *testing.T) {
	s, err := New(Config{
		Addr:         ":0",
		BaseURL:      "http://example.test",
		DatabasePath: filepath.Join(t.TempDir(), "font.db"),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	tests := []struct {
		path string
		want string
	}{
		{"/static/vendor/fonts/webfonts/Vazirmatn-400.ttf", "font/ttf"},
		{"/static/vendor/fonts/webfonts/Vazirmatn-Bold.woff2", "font/woff2"},
		{"/static/vendor/fontawesome.min.css", "text/css; charset=utf-8"},
	}
	for _, tt := range tests {
		req := httptest.NewRequest(http.MethodGet, tt.path, nil)
		rec := httptest.NewRecorder()
		s.http.Handler.ServeHTTP(rec, req)
		if rec.Code != http.StatusOK {
			t.Errorf("%s: status %d", tt.path, rec.Code)
			continue
		}
		if ct := rec.Header().Get("Content-Type"); ct != tt.want {
			t.Errorf("%s: Content-Type = %q, want %q", tt.path, ct, tt.want)
		}
	}
}
