package webui

import (
	"io"
	"net/http"
	"net/http/cookiejar"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"

	"mailgoserver/internal/pgp"
)

// TestWebmailComposeFrictionFixesFullJourney is a live-HTTP smoke test (real
// httptest.NewServer + a cookie-jar client, not raw httptest.NewRecorder calls) that
// walks the exact journey a user would take through all six friction-fix milestones
// in one continuous session: generate a passwordless S/MIME cert, fail a send and
// see the real error with content preserved, save a draft, reopen and re-save it,
// pick a PGP recipient from the dropdown (not by address auto-match), and finally
// send successfully. Session cookies flow exactly as a browser would send them.
func TestWebmailComposeFrictionFixesFullJourney(t *testing.T) {
	app := newTestApp(t)
	srv := httptest.NewServer(app.Mux())
	defer srv.Close()

	domains, _ := app.DB.ListDomains()
	domainID := domains[0].ID
	senderID := createTestMailboxWithPassword(t, app, "journey-sender@example.com", domainID, "sender-password-1!")
	recipID := createTestMailboxWithPassword(t, app, "journey-recip@example.com", domainID, "recip-password-1!")

	jar, err := cookiejar.New(nil)
	if err != nil {
		t.Fatal(err)
	}
	client := &http.Client{
		Jar: jar,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse // inspect redirects ourselves, like a test proxy would
		},
	}

	post := func(path string, form url.Values) *http.Response {
		t.Helper()
		resp, err := client.PostForm(srv.URL+path, form)
		if err != nil {
			t.Fatal(err)
		}
		return resp
	}
	get := func(path string) (*http.Response, string) {
		t.Helper()
		resp, err := client.Get(srv.URL + path)
		if err != nil {
			t.Fatal(err)
		}
		body, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		return resp, string(body)
	}

	// --- Login ---
	loginResp := post(MailboxPrefix+"/login", url.Values{"email": {"journey-sender@example.com"}, "password": {"sender-password-1!"}})
	loginResp.Body.Close()
	if loginResp.StatusCode != http.StatusFound {
		t.Fatalf("login: status=%d", loginResp.StatusCode)
	}

	// --- Milestone 1: generate an S/MIME cert with NO passphrase fields at all ---
	genResp := post(MailboxPrefix+"/smime/identity/generate", url.Values{})
	genResp.Body.Close()
	if genResp.StatusCode != http.StatusFound {
		t.Fatalf("smime generate: status=%d", genResp.StatusCode)
	}
	identities, err := app.DB.ListSMIMEIdentities(senderID)
	if err != nil || len(identities) != 1 {
		t.Fatalf("expected 1 S/MIME identity, got %d (err=%v)", len(identities), err)
	}

	// --- Milestones 2+3: a rejected send (missing subject) shows the specific
	// validation error rather than a generic message, and redisplays the form with
	// the already-typed body still filled in instead of wiping it via a redirect ---
	failResp := post(MailboxPrefix+"/mail/compose", url.Values{
		"to": {"journey-recip@example.com"}, "subject": {""}, "body_html": {"partially written thought"},
	})
	failBody, _ := io.ReadAll(failResp.Body)
	failResp.Body.Close()
	if failResp.StatusCode != http.StatusOK {
		t.Fatalf("expected the failed send to redisplay the form (200), got %d", failResp.StatusCode)
	}
	if !strings.Contains(string(failBody), "partially written thought") {
		t.Fatalf("expected the typed body preserved after a failed send, got: %s", failBody)
	}
	if !strings.Contains(string(failBody), "Please add a subject") {
		t.Fatalf("expected the specific validation error shown, got: %s", failBody)
	}

	// --- Milestone 4: save a draft, reopen it, re-save it (no duplicate) ---
	post(MailboxPrefix+"/mail/save-draft", url.Values{"subject": {"Draft subject"}, "body_html": {"draft body"}}).Body.Close()
	drafts, err := app.DB.ListMessagesInFolder(senderID, "Drafts")
	if err != nil || len(drafts) != 1 {
		t.Fatalf("expected 1 draft, got %d (err=%v)", len(drafts), err)
	}
	draftID := drafts[0].ID

	_, openBody := get(MailboxPrefix + "/mail/compose?draft=" + strconv.FormatInt(draftID, 10) + "&folder=Drafts")
	if !strings.Contains(openBody, "Draft subject") || !strings.Contains(openBody, "draft body") {
		t.Fatalf("expected the reopened draft prefilled, got: %s", openBody)
	}

	post(MailboxPrefix+"/mail/save-draft", url.Values{
		"subject": {"Draft subject"}, "body_html": {"draft body, edited"}, "draft_id": {strconv.FormatInt(draftID, 10)},
	}).Body.Close()
	drafts, err = app.DB.ListMessagesInFolder(senderID, "Drafts")
	if err != nil || len(drafts) != 1 {
		t.Fatalf("expected still 1 draft after re-save (no duplicate), got %d (err=%v)", len(drafts), err)
	}
	draftID = drafts[0].ID

	// --- Milestone 5: PGP encrypt using the recipient picker (not address matching)
	// — the sender needs their own PGP key too, and the recipient's key is filed as a
	// contact under a DIFFERENT email than the actual To address, proving the picker
	// (not auto-match-by-address) is what makes this work.
	senderPub, senderPriv, err := pgp.GenerateKeyPair("journey-sender@example.com", "sender pgp pass")
	if err != nil {
		t.Fatal(err)
	}
	if err := app.storePGPIdentity(senderID, "", "journey-sender@example.com", senderPub, senderPriv); err != nil {
		t.Fatal(err)
	}
	recipPub, _, err := pgp.GenerateKeyPair("journey-recip@example.com", "recip pgp pass")
	if err != nil {
		t.Fatal(err)
	}
	entity, err := pgp.ParsePublicKey(recipPub)
	if err != nil {
		t.Fatal(err)
	}
	if err := app.DB.UpsertPGPContact(senderID, "filed-under-a-different-address@example.com", "Journey recipient", pgp.Fingerprint(entity), string(recipPub)); err != nil {
		t.Fatal(err)
	}
	contact, err := app.DB.GetPGPContact(senderID, "filed-under-a-different-address@example.com")
	if err != nil || contact == nil {
		t.Fatal(err)
	}

	sendResp := post(MailboxPrefix+"/mail/compose", url.Values{
		"to": {"journey-recip@example.com"}, "subject": {"Final message"}, "body_html": {"the actual content"},
		"pgp_encrypt": {"1"}, "pgp_recipient_id": {strconv.FormatInt(contact.ID, 10)},
		"draft_id": {strconv.FormatInt(draftID, 10)},
	})
	sendBody, _ := io.ReadAll(sendResp.Body)
	sendResp.Body.Close()
	if sendResp.StatusCode != http.StatusFound {
		t.Fatalf("expected the send to succeed, got status=%d body=%s", sendResp.StatusCode, sendBody)
	}

	msgs, err := app.DB.ListMessagesInFolder(recipID, "INBOX")
	if err != nil || len(msgs) != 1 {
		t.Fatalf("expected 1 message delivered, got %d (err=%v)", len(msgs), err)
	}
	raw, err := app.Mailstore.FetchMessage(recipID, msgs[0].ID)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(raw), "the actual content") {
		t.Fatal("expected the body encrypted, not plaintext, in the stored message")
	}

	// Sending with draft_id set removes the draft, same as any real mail client.
	remainingDrafts, err := app.DB.ListMessagesInFolder(senderID, "Drafts")
	if err != nil || len(remainingDrafts) != 0 {
		t.Fatalf("expected the draft gone after sending, got %d (err=%v)", len(remainingDrafts), err)
	}

	// --- Milestone 6: the Mail view renders the compose popup widget, wired to the
	// real openCompose(...) calls, not the old plain navigation links. ---
	_, folderBody := get(MailboxPrefix + "/mail/INBOX")
	if !strings.Contains(folderBody, "openCompose('/webmail/mail/compose')") {
		t.Fatalf("expected the Compose button wired to the popup widget, got: %s", folderBody)
	}
	if !strings.Contains(folderBody, "composePopup") {
		t.Fatalf("expected the compose popup widget markup present on the mail view, got: %s", folderBody)
	}
}
