package webui

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"mailgoserver/internal/db"
	"mailgoserver/internal/mailstore"
)

// createTestMailboxWithPassword mirrors createMailboxFor but with a known plaintext
// portal password, for webmail login tests.
func createTestMailboxWithPassword(t *testing.T, app *App, email string, domainID int64, password string) int64 {
	t.Helper()
	hash, err := db.HashPassword(password)
	if err != nil {
		t.Fatal(err)
	}
	dek := mailstore.GenerateDEK()
	wrapped, nonce, err := app.Mailstore.WrapDEK(dek)
	if err != nil {
		t.Fatal(err)
	}
	id, err := app.DB.CreateMailbox(email, hash, domainID, 5*1024*1024*1024, wrapped, nonce)
	if err != nil {
		t.Fatal(err)
	}
	return id
}

func TestWebmailLoginSucceedsAndReachesDashboard(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "portaluser@example.com", domains[0].ID, "portal-password-123!")
	_ = mailboxID

	form := url.Values{"email": {"portaluser@example.com"}, "password": {"portal-password-123!"}}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect after login, got %d: %s", rec.Code, rec.Body.String())
	}
	var sessionCookie *http.Cookie
	for _, c := range rec.Result().Cookies() {
		if c.Name == mailboxSessionCookieName {
			sessionCookie = c
		}
	}
	if sessionCookie == nil {
		t.Fatal("expected a mailbox session cookie to be set")
	}

	// The webmail root is the mailbox itself now, not account settings — it redirects
	// straight to the inbox.
	req2 := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/", nil)
	req2.AddCookie(sessionCookie)
	rec2 := httptest.NewRecorder()
	mux.ServeHTTP(rec2, req2)
	if rec2.Code != http.StatusFound || rec2.Header().Get("Location") != MailboxPrefix+"/mail/INBOX" {
		t.Fatalf("expected the webmail root to redirect to the inbox, got %d Location=%q", rec2.Code, rec2.Header().Get("Location"))
	}

	req3 := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/account", nil)
	req3.AddCookie(sessionCookie)
	rec3 := httptest.NewRecorder()
	mux.ServeHTTP(rec3, req3)
	if rec3.Code != http.StatusOK {
		t.Fatalf("expected the account page to render, got %d: %s", rec3.Code, rec3.Body.String())
	}
	if !strings.Contains(rec3.Body.String(), "portaluser@example.com") {
		t.Fatal("expected the account page to show the mailbox's own email")
	}
}

func TestWebmailLoginRejectsAppPassword(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "portaluser2@example.com", domains[0].ID, "portal-password-123!")

	appPwHash, err := db.HashPassword("an-app-password-not-the-portal-one")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := app.DB.CreateAppPassword(mailboxID, "test", appPwHash, nil); err != nil {
		t.Fatal(err)
	}

	form := url.Values{"email": {"portaluser2@example.com"}, "password": {"an-app-password-not-the-portal-one"}}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Incorrect") {
		t.Fatalf("expected login to reject an app password (portal login only accepts the portal password), got status %d: %s", rec.Code, rec.Body.String())
	}
}

func TestWebmailUnauthenticatedRedirectsToLogin(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()

	req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect, got %d", rec.Code)
	}
	if loc := rec.Header().Get("Location"); !strings.HasPrefix(loc, MailboxPrefix+"/login") {
		t.Fatalf("expected redirect to webmail login, got %q", loc)
	}
}

// TestWebmailAndAdminSessionsAreIsolated confirms the two session systems really are
// separate: an admin session cookie doesn't grant webmail access and vice versa.
func TestWebmailAndAdminSessionsAreIsolated(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	createTestMailboxWithPassword(t, app, "portaluser3@example.com", domains[0].ID, "portal-password-123!")

	adminCookie := loginSession(t, app)
	req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/", nil)
	req.AddCookie(&http.Cookie{Name: mailboxSessionCookieName, Value: adminCookie.Value})
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected an admin session token to NOT grant webmail access, got status %d", rec.Code)
	}

	form := url.Values{"email": {"portaluser3@example.com"}, "password": {"portal-password-123!"}}
	loginReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
	loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	loginRec := httptest.NewRecorder()
	mux.ServeHTTP(loginRec, loginReq)
	var mailboxCookie *http.Cookie
	for _, c := range loginRec.Result().Cookies() {
		if c.Name == mailboxSessionCookieName {
			mailboxCookie = c
		}
	}
	if mailboxCookie == nil {
		t.Fatal("expected a mailbox session cookie")
	}
	req2 := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil)
	req2.AddCookie(&http.Cookie{Name: sessionCookieName, Value: mailboxCookie.Value})
	rec2 := httptest.NewRecorder()
	mux.ServeHTTP(rec2, req2)
	if rec2.Code != http.StatusFound {
		t.Fatalf("expected a mailbox session token to NOT grant admin access, got status %d", rec2.Code)
	}
}

func TestWebmailChangePassword(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "portaluser4@example.com", domains[0].ID, "old-password-123!")

	cookie := webmailLoginSession(t, app, mailboxID)
	form := url.Values{
		"current_password":     {"old-password-123!"},
		"new_password":         {"new-password-456!"},
		"new_password_confirm": {"new-password-456!"},
	}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/account/password", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect after password change, got %d: %s", rec.Code, rec.Body.String())
	}

	mbox, err := app.DB.GetMailboxByID(mailboxID)
	if err != nil {
		t.Fatal(err)
	}
	if !db.CheckPassword("new-password-456!", mbox.PasswordHash) {
		t.Fatal("expected the new password to have been saved")
	}
}

func TestWebmailAppPasswordSelfService(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "portaluser5@example.com", domains[0].ID, "password-123!")
	cookie := webmailLoginSession(t, app, mailboxID)

	form := url.Values{"label": {"my laptop"}}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/account/apppasswords/add", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect after creating app password, got %d: %s", rec.Code, rec.Body.String())
	}

	passwords, err := app.DB.ListAppPasswordsForMailbox(mailboxID)
	if err != nil || len(passwords) != 1 {
		t.Fatalf("expected exactly 1 app password, got %d (err=%v)", len(passwords), err)
	}

	revokeReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/account/apppasswords/"+itoa(passwords[0].ID)+"/revoke", nil)
	revokeReq.AddCookie(cookie)
	revokeRec := httptest.NewRecorder()
	mux.ServeHTTP(revokeRec, revokeReq)
	if revokeRec.Code != http.StatusFound {
		t.Fatalf("expected redirect after revoking, got %d", revokeRec.Code)
	}
	remaining, err := app.DB.ListAppPasswordsForMailbox(mailboxID)
	if err != nil || len(remaining) != 0 {
		t.Fatalf("expected 0 app passwords after revoke, got %d (err=%v)", len(remaining), err)
	}
}

func TestWebmailMFAGateRequiresCodeAfterTOTPEnabled(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "portaluser6@example.com", domains[0].ID, "password-123!")
	if err := app.DB.SetMailboxTOTPSecret(mailboxID, "JBSWY3DPEHPK3PXP", true); err != nil {
		t.Fatal(err)
	}

	form := url.Values{"email": {"portaluser6@example.com"}, "password": {"password-123!"}}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("expected redirect to MFA step, got %d", rec.Code)
	}
	loc := rec.Header().Get("Location")
	if loc != MailboxPrefix+"/login/mfa" {
		t.Fatalf("expected redirect to %s, got %q", MailboxPrefix+"/login/mfa", loc)
	}

	// No fully-verified session cookie should exist yet — only the pending-MFA cookie.
	for _, c := range rec.Result().Cookies() {
		if c.Name == mailboxSessionCookieName {
			t.Fatal("a fully-verified session must not be issued before MFA is satisfied")
		}
	}
}

// webmailLoginSession creates a fully-verified (no MFA enrolled) mailbox session
// directly via the DB, mirroring loginSession's admin equivalent.
func webmailLoginSession(t *testing.T, app *App, mailboxID int64) *http.Cookie {
	t.Helper()
	token, err := app.DB.CreateMailboxSession(mailboxID, true, sessionTTL)
	if err != nil {
		t.Fatal(err)
	}
	return &http.Cookie{Name: mailboxSessionCookieName, Value: token}
}
