package webui

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

// TestWebmailRulesAddAndRemove confirms a mailbox owner can create a filter rule for
// their own mailbox through the self-service portal and remove it again — the same
// underlying CRUD the admin-side page already uses.
func TestWebmailRulesAddAndRemove(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "ruler@example.com", domains[0].ID, "ruler-password-1!")
	cookie := webmailLoginSession(t, app, mailboxID)

	form := "priority=0&condition_field=subject&condition_op=contains&condition_value=newsletter&action=move_to_folder&action_value=Newsletters"
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/save", strings.NewReader(form))
	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("add rule: status=%d body=%s", rec.Code, rec.Body.String())
	}

	rules, err := app.DB.ListRulesForMailbox(mailboxID)
	if err != nil || len(rules) != 1 {
		t.Fatalf("expected 1 rule, got %d (err=%v)", len(rules), err)
	}
	if rules[0].ConditionValue != "newsletter" || rules[0].ActionValue != "Newsletters" {
		t.Errorf("unexpected rule: %+v", rules[0])
	}

	// It actually takes effect at delivery time (reusing mailstore.ApplyRules,
	// exercised in internal/smtpserver's own tests) — here just confirm the list page
	// renders it and removal works.
	listReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/rules", nil)
	listReq.AddCookie(cookie)
	listRec := httptest.NewRecorder()
	mux.ServeHTTP(listRec, listReq)
	if listRec.Code != http.StatusOK || !strings.Contains(listRec.Body.String(), "Newsletters") {
		t.Fatalf("expected the rule listed on the rules page, status=%d", listRec.Code)
	}

	rmReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/"+strconv.FormatInt(rules[0].ID, 10)+"/remove", nil)
	rmReq.AddCookie(cookie)
	rmRec := httptest.NewRecorder()
	mux.ServeHTTP(rmRec, rmReq)
	if rmRec.Code != http.StatusFound {
		t.Fatalf("remove rule: status=%d", rmRec.Code)
	}
	remaining, err := app.DB.ListRulesForMailbox(mailboxID)
	if err != nil || len(remaining) != 0 {
		t.Fatalf("expected no rules left, got %d (err=%v)", len(remaining), err)
	}
}

// TestWebmailRulesAddMultiCondition confirms the self-service rule builder's
// parallel condition_field/op/value arrays are correctly parsed into a
// multi-condition rule with the chosen match type.
func TestWebmailRulesAddMultiCondition(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "ruler3@example.com", domains[0].ID, "ruler-password-1!")
	cookie := webmailLoginSession(t, app, mailboxID)

	form := url.Values{
		"priority":        {"0"},
		"match_type":      {"any"},
		"condition_field": {"from", "subject"},
		"condition_op":    {"contains", "contains"},
		"condition_value": {"boss@work.example", "urgent"},
		"action":          {"mark_as_spam"},
		"action_value":    {""},
	}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/save", 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("add multi-condition rule: status=%d body=%s", rec.Code, rec.Body.String())
	}

	rules, err := app.DB.ListRulesForMailbox(mailboxID)
	if err != nil || len(rules) != 1 {
		t.Fatalf("expected 1 rule, got %d (err=%v)", len(rules), err)
	}
	conditions, matchType := rules[0].Conditions()
	if matchType != "any" || len(conditions) != 2 {
		t.Fatalf("expected 2 OR conditions, got matchType=%q conditions=%+v", matchType, conditions)
	}
	if rules[0].Action != "mark_as_spam" {
		t.Fatalf("expected mark_as_spam action, got %q", rules[0].Action)
	}
}

// TestWebmailRulesRejectsInvalidInput confirms a malformed rule submission is
// rejected rather than silently stored.
func TestWebmailRulesRejectsInvalidInput(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "ruler2@example.com", domains[0].ID, "ruler-password-1!")
	cookie := webmailLoginSession(t, app, mailboxID)

	// move_to_folder with no destination folder named.
	form := "priority=0&condition_field=subject&condition_op=contains&condition_value=x&action=move_to_folder&action_value="
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/save", strings.NewReader(form))
	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("status=%d", rec.Code)
	}
	rules, err := app.DB.ListRulesForMailbox(mailboxID)
	if err != nil || len(rules) != 0 {
		t.Fatalf("expected the invalid rule rejected, got %d rules (err=%v)", len(rules), err)
	}
}

// TestWebmailRulesEditUpdatesInPlace confirms submitting the builder form with an
// existing rule_id updates that rule (via UpdateRuleMulti) rather than creating a
// second one — the same form/endpoint (/rules/save) serves both add and edit.
func TestWebmailRulesEditUpdatesInPlace(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "ruler4@example.com", domains[0].ID, "ruler-password-1!")
	cookie := webmailLoginSession(t, app, mailboxID)

	ruleID, err := app.DB.CreateRule(mailboxID, 0, "subject", "contains", "old", "delete", "")
	if err != nil {
		t.Fatal(err)
	}

	form := url.Values{
		"rule_id":         {strconv.FormatInt(ruleID, 10)},
		"name":            {"Renamed"},
		"priority":        {"5"},
		"match_type":      {"all"},
		"condition_field": {"subject"},
		"condition_op":    {"contains"},
		"condition_value": {"new"},
		"action":          {"mark_read"},
		"action_value":    {""},
	}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/save", 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("edit rule: status=%d body=%s", rec.Code, rec.Body.String())
	}

	rules, err := app.DB.ListRulesForMailbox(mailboxID)
	if err != nil || len(rules) != 1 {
		t.Fatalf("expected still exactly 1 rule (updated, not duplicated), got %d (err=%v)", len(rules), err)
	}
	if rules[0].ID != ruleID || rules[0].Name != "Renamed" || rules[0].Action != "mark_read" || rules[0].ConditionValue != "new" {
		t.Errorf("unexpected rule after edit: %+v", rules[0])
	}
}

// TestWebmailRulesEditPersistsEnabledCheckbox reproduces a live bug: the edit
// builder's "Enabled" checkbox visually reflected a disabled rule's state but
// submitting an edit never actually persisted it — UpdateRuleMulti's column list
// never included is_active, so editing (with or without touching the checkbox) never
// changed enabled/disabled state at all, only the separate toggle button did.
func TestWebmailRulesEditPersistsEnabledCheckbox(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "ruler6@example.com", domains[0].ID, "ruler-password-1!")
	cookie := webmailLoginSession(t, app, mailboxID)

	ruleID, err := app.DB.CreateRule(mailboxID, 0, "subject", "contains", "x", "delete", "")
	if err != nil {
		t.Fatal(err)
	}
	if err := app.DB.SetRuleActive(ruleID, mailboxID, false); err != nil {
		t.Fatal(err)
	}

	// Edit the disabled rule with the "Enabled" checkbox checked (is_active=1 present
	// in the form) — should re-enable it.
	form := url.Values{
		"rule_id": {strconv.FormatInt(ruleID, 10)}, "priority": {"0"}, "match_type": {"all"},
		"condition_field": {"subject"}, "condition_op": {"contains"}, "condition_value": {"x"},
		"action": {"delete"}, "action_value": {""}, "is_active": {"1"},
	}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/save", 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("edit rule: status=%d", rec.Code)
	}
	rule, err := app.DB.GetRuleByID(mailboxID, ruleID)
	if err != nil || rule == nil || !rule.IsActive {
		t.Fatalf("expected rule re-enabled via edit form's checkbox, got %+v (err=%v)", rule, err)
	}

	// Edit again with the checkbox omitted entirely (as a real unchecked <input
	// type=checkbox> submits) — should disable it.
	form.Del("is_active")
	req = httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/save", 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("edit rule: status=%d", rec.Code)
	}
	rule, err = app.DB.GetRuleByID(mailboxID, ruleID)
	if err != nil || rule == nil || rule.IsActive {
		t.Fatalf("expected rule disabled via edit form's omitted checkbox, got %+v (err=%v)", rule, err)
	}
}

// TestWebmailRulesToggleFlipsActive confirms the quick enable/disable toggle button
// flips is_active without needing the full edit form.
func TestWebmailRulesToggleFlipsActive(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "ruler5@example.com", domains[0].ID, "ruler-password-1!")
	cookie := webmailLoginSession(t, app, mailboxID)

	ruleID, err := app.DB.CreateRule(mailboxID, 0, "subject", "contains", "x", "delete", "")
	if err != nil {
		t.Fatal(err)
	}

	toggle := func() {
		req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/"+strconv.FormatInt(ruleID, 10)+"/toggle", nil)
		req.AddCookie(cookie)
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		if rec.Code != http.StatusFound {
			t.Fatalf("toggle: status=%d", rec.Code)
		}
	}

	toggle()
	rules, _ := app.DB.ListRulesForMailbox(mailboxID)
	if rules[0].IsActive {
		t.Fatalf("expected rule disabled after first toggle, got IsActive=true")
	}
	toggle()
	rules, _ = app.DB.ListRulesForMailbox(mailboxID)
	if !rules[0].IsActive {
		t.Fatalf("expected rule re-enabled after second toggle, got IsActive=false")
	}
}

// TestWebmailRulesScopedToOwnMailbox confirms one mailbox owner can't remove another
// mailbox's rule by guessing its ID.
func TestWebmailRulesScopedToOwnMailbox(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	victimID := createTestMailboxWithPassword(t, app, "victim2@example.com", domains[0].ID, "victim-password-1!")
	attackerID := createTestMailboxWithPassword(t, app, "attacker2@example.com", domains[0].ID, "attacker-password-1!")

	ruleID, err := app.DB.CreateRule(victimID, 0, "subject", "contains", "x", "delete", "")
	if err != nil {
		t.Fatal(err)
	}

	attackerCookie := webmailLoginSession(t, app, attackerID)
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/rules/"+strconv.FormatInt(ruleID, 10)+"/remove", nil)
	req.AddCookie(attackerCookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("status=%d", rec.Code)
	}

	stillThere, err := app.DB.ListRulesForMailbox(victimID)
	if err != nil || len(stillThere) != 1 {
		t.Fatalf("expected the victim's rule untouched, got %d (err=%v)", len(stillThere), err)
	}
}
