//go:build integration

package httpx

import (
	"bytes"
	"mime/multipart"
	"net/http"
	"strings"
	"testing"

	"bountyboard/internal/store"
)

func TestMessagingFlow(t *testing.T) {
	ts, _, _, _ := newAuthStack(t, nil)

	a := newClient(t)
	userA := registerUser(t, ts.URL, a, "alice@example.com", "Alice A")
	aCSRF := csrfFrom(t, a, ts.URL)
	b := newClient(t)
	userB := registerUser(t, ts.URL, b, "bob@example.com", "Bob B")
	bCSRF := csrfFrom(t, b, ts.URL)
	c := newClient(t)
	registerUser(t, ts.URL, c, "carol@example.com", "Carol C")

	// Alice opens a DM with Bob
	resp := mustPost(t, a, ts.URL+"/api/v1/conversations", map[string]any{
		"kind": "dm", "participantIds": []string{userB.ID},
	}, aCSRF, http.StatusOK)
	var created struct {
		Conversation store.Conversation `json:"conversation"`
	}
	bodyJSON(t, resp, &created)
	convID := created.Conversation.ID

	// dm is unique: creating again returns the same conversation
	resp = mustPost(t, a, ts.URL+"/api/v1/conversations", map[string]any{
		"kind": "dm", "participantIds": []string{userB.ID},
	}, aCSRF, http.StatusOK)
	var again struct {
		Conversation store.Conversation `json:"conversation"`
	}
	bodyJSON(t, resp, &again)
	if again.Conversation.ID != convID {
		t.Fatalf("dm not deduplicated: %s vs %s", again.Conversation.ID, convID)
	}

	// Alice sends a rich-text message; script is sanitized away
	resp = mustPost(t, a, ts.URL+"/api/v1/conversations/"+convID+"/messages", map[string]any{
		"body": `<p>Hi <b>Bob</b>!</p><script>alert(1)</script>`,
	}, aCSRF, http.StatusCreated)
	var sent struct {
		Message store.Message `json:"message"`
	}
	bodyJSON(t, resp, &sent)
	if strings.Contains(sent.Message.Body, "<script") || !strings.Contains(sent.Message.Body, "<b>Bob</b>") {
		t.Fatalf("sanitization wrong: %q", sent.Message.Body)
	}

	// Bob sees 1 unread; Carol sees nothing
	resp, err := b.Get(ts.URL + "/api/v1/conversations")
	if err != nil {
		t.Fatal(err)
	}
	var bobConvs struct {
		Conversations []map[string]any `json:"conversations"`
	}
	bodyJSON(t, resp, &bobConvs)
	if len(bobConvs.Conversations) != 1 {
		t.Fatalf("bob conversations: %d", len(bobConvs.Conversations))
	}
	if got := bobConvs.Conversations[0]["unread"].(float64); got != 1 {
		t.Fatalf("bob unread = %v, want 1", got)
	}
	if title := bobConvs.Conversations[0]["title"]; title != "Alice A" {
		t.Fatalf("dm title for bob = %v", title)
	}

	// Carol can't read or post
	cResp, err := c.Get(ts.URL + "/api/v1/conversations/" + convID + "/messages")
	if err != nil {
		t.Fatal(err)
	}
	cResp.Body.Close()
	if cResp.StatusCode != http.StatusForbidden {
		t.Fatalf("carol read: %d", cResp.StatusCode)
	}

	// Bob reads → unread 0
	mustPost(t, b, ts.URL+"/api/v1/conversations/"+convID+"/read", map[string]any{}, bCSRF, http.StatusOK).Body.Close()
	resp, _ = b.Get(ts.URL + "/api/v1/conversations")
	bodyJSON(t, resp, &bobConvs)
	if got := bobConvs.Conversations[0]["unread"].(float64); got != 0 {
		t.Fatalf("bob unread after read = %v", got)
	}

	// Bob replies with a file attachment
	up := uploadChatFile(t, b, ts.URL, bCSRF, "shot.png", pngBytes)
	resp = mustPost(t, b, ts.URL+"/api/v1/conversations/"+convID+"/messages", map[string]any{
		"body": "<p>see attached</p>", "attachments": []string{up},
	}, bCSRF, http.StatusCreated)
	bodyJSON(t, resp, &sent)
	if len(sent.Message.Attachments) != 1 || !sent.Message.Attachments[0].IsImage {
		t.Fatalf("attachment: %+v", sent.Message.Attachments)
	}

	// Alice (participant) can fetch the chat file; Carol cannot
	fResp, _ := a.Get(ts.URL + "/files/" + up)
	fResp.Body.Close()
	if fResp.StatusCode != http.StatusOK {
		t.Fatalf("alice chat file: %d", fResp.StatusCode)
	}
	fResp, _ = c.Get(ts.URL + "/files/" + up)
	fResp.Body.Close()
	if fResp.StatusCode != http.StatusForbidden {
		t.Fatalf("carol chat file: %d, want 403", fResp.StatusCode)
	}

	// history pagination shape: both messages, chronological
	resp, _ = a.Get(ts.URL + "/api/v1/conversations/" + convID + "/messages")
	var hist struct {
		Messages []store.Message `json:"messages"`
	}
	bodyJSON(t, resp, &hist)
	if len(hist.Messages) != 2 || hist.Messages[0].SenderID != userA.ID {
		t.Fatalf("history: %d messages, first by %s", len(hist.Messages), hist.Messages[0].SenderID)
	}

	// group conversation requires a title
	resp = postJSON(t, a, ts.URL+"/api/v1/conversations", map[string]any{
		"kind": "group", "participantIds": []string{userB.ID},
	}, aCSRF)
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("untitled group: %d", resp.StatusCode)
	}
	resp.Body.Close()
	mustPost(t, a, ts.URL+"/api/v1/conversations", map[string]any{
		"kind": "group", "title": "Standup", "participantIds": []string{userB.ID},
	}, aCSRF, http.StatusCreated).Body.Close()
}

func uploadChatFile(t *testing.T, c *http.Client, ts, csrf, name string, content []byte) string {
	t.Helper()
	var buf bytes.Buffer
	mw := multipart.NewWriter(&buf)
	fw, err := mw.CreateFormFile("file", name)
	if err != nil {
		t.Fatal(err)
	}
	fw.Write(content)
	mw.Close()
	req, _ := http.NewRequest(http.MethodPost, ts+"/api/v1/files", &buf)
	req.Header.Set("Content-Type", mw.FormDataContentType())
	req.Header.Set(csrfHeader, csrf)
	resp, err := c.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("upload: %d", resp.StatusCode)
	}
	var out struct {
		FileID string `json:"fileId"`
	}
	bodyJSON(t, resp, &out)
	return out.FileID
}

func TestMentionInChatNotifies(t *testing.T) {
	ts, _, st, _ := newAuthStack(t, nil)
	a := newClient(t)
	registerUser(t, ts.URL, a, "ma@example.com", "Mention Alice")
	aCSRF := csrfFrom(t, a, ts.URL)
	b := newClient(t)
	userB := registerUser(t, ts.URL, b, "mb@example.com", "Mention Bob")

	resp := mustPost(t, a, ts.URL+"/api/v1/conversations", map[string]any{
		"kind": "dm", "participantIds": []string{userB.ID},
	}, aCSRF, http.StatusOK)
	var created struct {
		Conversation store.Conversation `json:"conversation"`
	}
	bodyJSON(t, resp, &created)

	mustPost(t, a, ts.URL+"/api/v1/conversations/"+created.Conversation.ID+"/messages",
		map[string]any{"body": "<p>hey @mention, look at this</p>"}, aCSRF, http.StatusCreated).Body.Close()

	notifs, err := st.ListNotifications(t.Context(), userB.ID, false, "", 20)
	if err != nil {
		t.Fatal(err)
	}
	var mentioned bool
	for _, n := range notifs {
		if n.Kind == "mention" {
			mentioned = true
		}
	}
	if !mentioned {
		t.Fatal("bob was not notified about the mention")
	}
}
