package media_test

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"net/textproto"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	. "github.com/tidbcloud/AgentTeam/internal/api/testkit"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

func TestAttachmentsUploadMessageAndDownload(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var auth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "member@example.com",
		"password": TestUserPassword,
		"name":     "member",
	}, nil, &auth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var channels []ChannelResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/channels", nil, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &channels)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, channels)

	var allChannelID string
	for _, channel := range channels {
		if channel.Name == "all" {
			allChannelID = channel.ID
			break
		}
	}
	require.NotEmpty(t, allChannelID)

	fileBody := []byte("hello attachment")

	var multipartBody bytes.Buffer
	writer := multipart.NewWriter(&multipartBody)
	require.NoError(t, writer.WriteField("scopeId", allChannelID))
	part, err := writer.CreateFormFile("files", "hello.txt")
	require.NoError(t, err)
	_, err = part.Write(fileBody)
	require.NoError(t, err)
	require.NoError(t, writer.Close())

	uploadRequest, err := http.NewRequestWithContext(context.Background(), http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/attachments", &multipartBody)
	require.NoError(t, err)
	uploadRequest.Header.Set("Content-Type", writer.FormDataContentType())
	uploadRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", auth.AccessToken))
	uploadRequest.Header.Set("X-Workspace-Id", workspaceID)

	uploadResponse, err := client.Do(uploadRequest)
	require.NoError(t, err)
	defer uploadResponse.Body.Close()
	require.Equal(t, http.StatusOK, uploadResponse.StatusCode)

	var uploaded struct {
		Attachments []attachment.AppAttachment `json:"attachments"`
	}
	require.NoError(t, json.NewDecoder(uploadResponse.Body).Decode(&uploaded))
	require.Len(t, uploaded.Attachments, 1)
	require.NotEmpty(t, uploaded.Attachments[0].ID)
	require.Equal(t, "hello.txt", uploaded.Attachments[0].Filename)

	downloadBeforeSend, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/attachments/"+uploaded.Attachments[0].ID, nil)
	require.NoError(t, err)
	downloadBeforeSend.Header.Set("Authorization", fmt.Sprintf("Bearer %s", auth.AccessToken))
	beforeSendResponse, err := client.Do(downloadBeforeSend)
	require.NoError(t, err)
	defer beforeSendResponse.Body.Close()
	require.Equal(t, http.StatusNotFound, beforeSendResponse.StatusCode)

	var appMessage message.AppMessage
	response = DoJSON(t, client, http.MethodPost, server.URL+WorkspaceScopeMessagesPath(workspaceID, allChannelID), map[string]any{
		"scopeId":       allChannelID,
		"content":       "",
		"attachmentIds": []string{uploaded.Attachments[0].ID},
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &appMessage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, allChannelID, appMessage.ChannelID)
	require.Equal(t, "", appMessage.Content)
	require.Len(t, appMessage.Attachments, 1)
	require.Equal(t, uploaded.Attachments[0].ID, appMessage.Attachments[0].ID)
	require.Equal(t, "hello.txt", appMessage.Attachments[0].Filename)

	var page message.AppMessagePage
	response = DoJSON(t, client, http.MethodGet, server.URL+WorkspaceScopeMessagesPathWithLimit(workspaceID, allChannelID, 50), nil, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &page)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, page.Messages)
	require.Equal(t, appMessage.ID, page.Messages[len(page.Messages)-1].ID)
	require.Len(t, page.Messages[len(page.Messages)-1].Attachments, 1)
	require.Equal(t, uploaded.Attachments[0].ID, page.Messages[len(page.Messages)-1].Attachments[0].ID)

	downloadRequest, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/attachments/"+uploaded.Attachments[0].ID, nil)
	require.NoError(t, err)
	downloadRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", auth.AccessToken))
	downloadResponse, err := client.Do(downloadRequest)
	require.NoError(t, err)
	defer downloadResponse.Body.Close()
	require.Equal(t, http.StatusOK, downloadResponse.StatusCode)

	downloadedBody, err := io.ReadAll(downloadResponse.Body)
	require.NoError(t, err)
	require.Equal(t, fileBody, downloadedBody)
}

func TestAttachmentsDownloadSupportsAgentAuthToken(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var auth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "member@example.com",
		"password": TestUserPassword,
		"name":     "member",
	}, nil, &auth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var channels []ChannelResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/channels", nil, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &channels)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, channels)

	var allChannelID string
	for _, channel := range channels {
		if channel.Name == "all" {
			allChannelID = channel.ID
			break
		}
	}
	require.NotEmpty(t, allChannelID)

	var machineResponse dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "dev-machine",
	}, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &machineResponse)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, machineResponse.Machine.ID)

	var createdAgent agent.AppAgent
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents", map[string]any{
		"name":      "attachment-agent",
		"runtime":   "codex",
		"machineId": machineResponse.Machine.ID,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &createdAgent)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, createdAgent.ID)
	MakeAgentPublicChannelParticipantForAppTest(t, runtime, workspaceID, allChannelID, createdAgent.ID)

	startCommand, err := runtime.Store.PrepareAgentStart(context.Background(), createdAgent.ID, server.URL, nil, true, true, false)
	require.NoError(t, err)
	require.NotEmpty(t, startCommand.Config.AuthToken)

	fileBody := []byte("hello attachment for agent")

	var multipartBody bytes.Buffer
	writer := multipart.NewWriter(&multipartBody)
	require.NoError(t, writer.WriteField("scopeId", allChannelID))
	part, err := writer.CreateFormFile("files", "hello.txt")
	require.NoError(t, err)
	_, err = part.Write(fileBody)
	require.NoError(t, err)
	require.NoError(t, writer.Close())

	uploadRequest, err := http.NewRequestWithContext(context.Background(), http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/attachments", &multipartBody)
	require.NoError(t, err)
	uploadRequest.Header.Set("Content-Type", writer.FormDataContentType())
	uploadRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", auth.AccessToken))
	uploadRequest.Header.Set("X-Workspace-Id", workspaceID)

	uploadResponse, err := client.Do(uploadRequest)
	require.NoError(t, err)
	defer uploadResponse.Body.Close()
	require.Equal(t, http.StatusOK, uploadResponse.StatusCode)

	var uploaded struct {
		Attachments []attachment.AppAttachment `json:"attachments"`
	}
	require.NoError(t, json.NewDecoder(uploadResponse.Body).Decode(&uploaded))
	require.Len(t, uploaded.Attachments, 1)

	var appMessage message.AppMessage
	response = DoJSON(t, client, http.MethodPost, server.URL+WorkspaceScopeMessagesPath(workspaceID, allChannelID), map[string]any{
		"scopeId":       allChannelID,
		"content":       "",
		"attachmentIds": []string{uploaded.Attachments[0].ID},
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &appMessage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, appMessage.Attachments, 1)

	downloadRequest, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodGet,
		server.URL+"/api/v1/workspaces/"+workspaceID+"/attachments/"+uploaded.Attachments[0].ID,
		nil,
	)
	require.NoError(t, err)
	downloadRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", startCommand.Config.AuthToken))
	downloadResponse, err := client.Do(downloadRequest)
	require.NoError(t, err)
	defer downloadResponse.Body.Close()
	require.Equal(t, http.StatusOK, downloadResponse.StatusCode)

	downloadedBody, err := io.ReadAll(downloadResponse.Body)
	require.NoError(t, err)
	require.Equal(t, fileBody, downloadedBody)
}

func TestPersonalStickersUploadListDownloadAndPrivacy(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var owner dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "sticker-owner@example.com",
		"password": TestUserPassword,
		"name":     "sticker-owner",
	}, nil, &owner)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", owner.AccessToken),
	}, &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var channels []ChannelResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/channels", nil, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", owner.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &channels)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, channels)

	var allChannelID string
	for _, channel := range channels {
		if channel.Name == "all" {
			allChannelID = channel.ID
			break
		}
	}
	require.NotEmpty(t, allChannelID)

	tinyPNG, err := base64.StdEncoding.DecodeString(
		"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+kfuoAAAAASUVORK5CYII=",
	)
	require.NoError(t, err)

	var multipartBody bytes.Buffer
	writer := multipart.NewWriter(&multipartBody)
	partHeader := make(textproto.MIMEHeader)
	partHeader.Set("Content-Disposition", `form-data; name="files"; filename="wave.png"`)
	partHeader.Set("Content-Type", "image/png")
	part, err := writer.CreatePart(partHeader)
	require.NoError(t, err)
	_, err = part.Write(tinyPNG)
	require.NoError(t, err)
	require.NoError(t, writer.Close())

	uploadRequest, err := http.NewRequestWithContext(context.Background(), http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/stickers", &multipartBody)
	require.NoError(t, err)
	uploadRequest.Header.Set("Content-Type", writer.FormDataContentType())
	uploadRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", owner.AccessToken))
	uploadRequest.Header.Set("X-Workspace-Id", workspaceID)

	uploadResponse, err := client.Do(uploadRequest)
	require.NoError(t, err)
	defer uploadResponse.Body.Close()
	require.Equal(t, http.StatusOK, uploadResponse.StatusCode)

	var uploaded struct {
		Stickers []attachment.PersonalSticker `json:"stickers"`
	}
	require.NoError(t, json.NewDecoder(uploadResponse.Body).Decode(&uploaded))
	require.Len(t, uploaded.Stickers, 1)
	require.NotEmpty(t, uploaded.Stickers[0].ID)
	require.Equal(t, "wave.png", uploaded.Stickers[0].Filename)
	require.Equal(t, "image/png", uploaded.Stickers[0].ContentType)

	var attachmentBody bytes.Buffer
	attachmentWriter := multipart.NewWriter(&attachmentBody)
	require.NoError(t, attachmentWriter.WriteField("scopeId", allChannelID))
	attachmentPartHeader := make(textproto.MIMEHeader)
	attachmentPartHeader.Set("Content-Disposition", `form-data; name="files"; filename="wave.png"`)
	attachmentPartHeader.Set("Content-Type", "image/png")
	attachmentPart, err := attachmentWriter.CreatePart(attachmentPartHeader)
	require.NoError(t, err)
	_, err = attachmentPart.Write(tinyPNG)
	require.NoError(t, err)
	require.NoError(t, attachmentWriter.Close())

	attachmentUploadRequest, err := http.NewRequestWithContext(context.Background(), http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/attachments", &attachmentBody)
	require.NoError(t, err)
	attachmentUploadRequest.Header.Set("Content-Type", attachmentWriter.FormDataContentType())
	attachmentUploadRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", owner.AccessToken))
	attachmentUploadRequest.Header.Set("X-Workspace-Id", workspaceID)

	attachmentUploadResponse, err := client.Do(attachmentUploadRequest)
	require.NoError(t, err)
	defer attachmentUploadResponse.Body.Close()
	require.Equal(t, http.StatusOK, attachmentUploadResponse.StatusCode)

	var uploadedAttachments struct {
		Attachments []attachment.AppAttachment `json:"attachments"`
	}
	require.NoError(t, json.NewDecoder(attachmentUploadResponse.Body).Decode(&uploadedAttachments))
	require.Len(t, uploadedAttachments.Attachments, 1)

	var stickerMessage message.AppMessage
	response = DoJSON(t, client, http.MethodPost, server.URL+WorkspaceScopeMessagesPath(workspaceID, allChannelID), map[string]any{
		"scopeId":       allChannelID,
		"content":       "",
		"attachmentIds": []string{uploadedAttachments.Attachments[0].ID},
		"messageType":   message.MessageTypeSticker,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", owner.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &stickerMessage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, message.MessageTypeSticker, stickerMessage.MessageType)
	require.Len(t, stickerMessage.Attachments, 1)

	var stickers []attachment.PersonalSticker
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/stickers", nil, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", owner.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &stickers)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, stickers, 1)
	require.Equal(t, uploaded.Stickers[0].ID, stickers[0].ID)

	downloadRequest, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodGet,
		server.URL+"/api/v1/workspaces/"+workspaceID+"/stickers/"+uploaded.Stickers[0].ID+"?token="+owner.AccessToken,
		nil,
	)
	require.NoError(t, err)
	downloadResponse, err := client.Do(downloadRequest)
	require.NoError(t, err)
	defer downloadResponse.Body.Close()
	require.Equal(t, http.StatusOK, downloadResponse.StatusCode)
	downloadedBody, err := io.ReadAll(downloadResponse.Body)
	require.NoError(t, err)
	require.Equal(t, tinyPNG, downloadedBody)

	var other dto.AuthTokensResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "sticker-other@example.com",
		"password": TestUserPassword,
		"name":     "sticker-other",
	}, nil, &other)
	require.Equal(t, http.StatusOK, response.StatusCode)

	forbiddenRequest, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodGet,
		server.URL+"/api/v1/workspaces/"+workspaceID+"/stickers/"+uploaded.Stickers[0].ID+"?token="+other.AccessToken,
		nil,
	)
	require.NoError(t, err)
	forbiddenResponse, err := client.Do(forbiddenRequest)
	require.NoError(t, err)
	defer forbiddenResponse.Body.Close()
	require.Equal(t, http.StatusNotFound, forbiddenResponse.StatusCode)

	var apiErr struct {
		Error string `json:"error"`
	}
	require.NoError(t, json.NewDecoder(forbiddenResponse.Body).Decode(&apiErr))
	require.Equal(t, "not found", apiErr.Error)

	deleteRequest, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodDelete,
		server.URL+"/api/v1/workspaces/"+workspaceID+"/stickers/"+uploaded.Stickers[0].ID,
		nil,
	)
	require.NoError(t, err)
	deleteRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", owner.AccessToken))
	deleteRequest.Header.Set("X-Workspace-Id", workspaceID)

	deleteResponse, err := client.Do(deleteRequest)
	require.NoError(t, err)
	defer deleteResponse.Body.Close()
	require.Equal(t, http.StatusNoContent, deleteResponse.StatusCode)

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/stickers", nil, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", owner.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &stickers)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, stickers)

	deletedDownloadRequest, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodGet,
		server.URL+"/api/v1/workspaces/"+workspaceID+"/stickers/"+uploaded.Stickers[0].ID+"?token="+owner.AccessToken,
		nil,
	)
	require.NoError(t, err)
	deletedDownloadResponse, err := client.Do(deletedDownloadRequest)
	require.NoError(t, err)
	defer deletedDownloadResponse.Body.Close()
	require.Equal(t, http.StatusNotFound, deletedDownloadResponse.StatusCode)
}
