// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: LicenseRef-MutuaL-1.2

package cooked

import (
	"context"
	"net/http"
	"strings"
	"testing"
)

func TestListRecipesACIDAPIClientResponses1AcceptsEmptyRecipesList(t *testing.T) {
	client, closeServer := newTestClient(t, rawRecipeListTestHandler(
		t,
		"/api/user/returned-user/recipes",
		`[]`,
	))
	defer closeServer()

	recipes, err := client.ListRecipes(context.Background(), 1, 10)
	if err != nil {
		t.Fatalf("ListRecipes() error = %v", err)
	}
	if len(recipes) != 0 {
		t.Fatalf("recipes length = %d, want 0", len(recipes))
	}
}

func TestListRecipesACIDAPIClientResponses1RejectsNullRecipesList(t *testing.T) {
	client, closeServer := newTestClient(t, rawRecipeListTestHandler(
		t,
		"/api/user/returned-user/recipes",
		`null`,
	))
	defer closeServer()

	_, err := client.ListRecipes(context.Background(), 1, 10)
	if err == nil {
		t.Fatal("ListRecipes() error = nil, want missing recipes error")
	}
}

func TestSearchRecipesACIDAPIClientResponses1RejectsRecipeCardsWithoutIDOrTitle(t *testing.T) {
	tests := []struct {
		name string
		body string
	}{
		{name: "missing id", body: `[{"title":"Pasta"}]`},
		{name: "blank title", body: `[{"id":"recipe-1","title":"   "}]`},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			client, closeServer := newTestClient(t, rawRecipeListTestHandler(
				t,
				"/api/user/returned-user/recipes/search",
				tt.body,
			))
			defer closeServer()

			_, err := client.SearchRecipes(context.Background(), "pasta", 1)
			if err == nil {
				t.Fatal("SearchRecipes() error = nil, want invalid recipe card error")
			}
		})
	}
}

// api-client.RESPONSES.1 recipes.SAVE.7 recipes.SAVE.9
func TestSaveRecipeDraftACIDAPIClientResponses1RejectsMissingRecipeID(t *testing.T) {
	var saveCalls int
	client, closeServer := newTestClient(t, rawSaveRecipeDraftTestHandler(t, &saveCalls, `{}`))
	defer closeServer()

	_, err := client.SaveRecipeDraft(
		context.Background(),
		"draft/with space",
		"# Pasta\n\n1. Boil pasta.",
		4,
	)
	if err == nil {
		t.Fatal("SaveRecipeDraft() error = nil, want missing recipe ID error")
	}
	if !strings.Contains(err.Error(), "missing recipe ID") {
		t.Fatalf("SaveRecipeDraft() error = %v, want missing recipe ID error", err)
	}
	if saveCalls != 1 {
		t.Fatalf("save calls = %d, want 1", saveCalls)
	}
}

// api-client.RESPONSES.1 recipes.SAVE.2 recipes.SAVE.9
func TestSavePreparedRecipeACIDAPIClientResponses1RejectsMissingOrBlankRecipeID(t *testing.T) {
	tests := []struct {
		name string
		body string
	}{
		{name: "missing recipe ID", body: `{}`},
		{name: "blank recipe ID", body: `{"recipe-id":"   "}`},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var saveCalls int
			client, closeServer := newTestClient(t, rawSavePreparedRecipeTestHandler(t, &saveCalls, tt.body))
			defer closeServer()

			_, err := client.SavePreparedRecipe(
				context.Background(),
				"Pasta",
				"# Pasta\n\n1. Boil pasta.",
				2,
			)
			if err == nil {
				t.Fatal("SavePreparedRecipe() error = nil, want missing recipe ID error")
			}
			if !strings.Contains(err.Error(), "missing recipe ID") {
				t.Fatalf("SavePreparedRecipe() error = %v, want missing recipe ID error", err)
			}
			if saveCalls != 1 {
				t.Fatalf("save calls = %d, want 1", saveCalls)
			}
		})
	}
}

// api-client.RESPONSES.1 recipes.READ.7 recipes.READ.8
func TestReadRecipeContentACIDAPIClientResponses1RejectsMissingContentOrPortions(t *testing.T) {
	tests := []struct {
		name    string
		body    string
		wantErr string
	}{
		{name: "missing content", body: `{"portions":2}`, wantErr: "missing content"},
		{name: "blank content", body: `{"content":"   ","portions":2}`, wantErr: "missing content"},
		{name: "missing portions", body: `{"content":"# Pasta"}`, wantErr: "missing or invalid portions"},
		{name: "zero portions", body: `{"content":"# Pasta","portions":0}`, wantErr: "missing or invalid portions"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var contentCalls int
			client, closeServer := newTestClient(t, rawRecipeContentTestHandler(t, &contentCalls, tt.body))
			defer closeServer()

			_, err := client.ReadRecipeContent(context.Background(), "recipe/with space")
			if err == nil {
				t.Fatal("ReadRecipeContent() error = nil, want invalid recipe content error")
			}
			if !strings.Contains(err.Error(), tt.wantErr) {
				t.Fatalf("ReadRecipeContent() error = %v, want %q", err, tt.wantErr)
			}
			if contentCalls != 1 {
				t.Fatalf("content calls = %d, want 1", contentCalls)
			}
		})
	}
}

func rawRecipeListTestHandler(t *testing.T, path, body string) http.HandlerFunc {
	t.Helper()

	return func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/public/login":
			writeLoginResponse(t, w)
		case path:
			w.Header().Set("Content-Type", "application/json")
			if _, err := w.Write([]byte(body)); err != nil {
				t.Fatalf("write raw recipe list response: %v", err)
			}
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}
}

func rawRecipeContentTestHandler(t *testing.T, contentCalls *int, body string) http.HandlerFunc {
	t.Helper()

	return func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.EscapedPath() {
		case "/api/public/login":
			writeLoginResponse(t, w)
		case "/api/recipe/recipe%2Fwith%20space/content":
			requireMethod(t, r, http.MethodGet, "content")
			requireSessionCookie(t, r)
			(*contentCalls)++
			w.Header().Set("Content-Type", "application/json")
			if _, err := w.Write([]byte(body)); err != nil {
				t.Fatalf("write raw recipe content response: %v", err)
			}
		default:
			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
		}
	}
}

func rawSavePreparedRecipeTestHandler(t *testing.T, saveCalls *int, body string) http.HandlerFunc {
	t.Helper()

	return func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/public/login":
			writeLoginResponse(t, w)
		case "/api/recipe/import/save":
			requireMethod(t, r, http.MethodPost, "save prepared recipe")
			requireSessionCookie(t, r)
			requireSavePreparedRecipeRequest(t, r)
			(*saveCalls)++
			w.Header().Set("Content-Type", "application/json")
			if _, err := w.Write([]byte(body)); err != nil {
				t.Fatalf("write raw save prepared response: %v", err)
			}
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}
}

func rawSaveRecipeDraftTestHandler(t *testing.T, saveCalls *int, body string) http.HandlerFunc {
	t.Helper()

	return func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.EscapedPath() {
		case "/api/public/login":
			writeLoginResponse(t, w)
		case "/api/extract/draft%2Fwith%20space/save":
			requireMethod(t, r, http.MethodPost, "save recipe draft")
			requireSessionCookie(t, r)
			requireRecipeContentRequest(t, r, "draft save")
			(*saveCalls)++
			w.Header().Set("Content-Type", "application/json")
			if _, err := w.Write([]byte(body)); err != nil {
				t.Fatalf("write raw save draft response: %v", err)
			}
		default:
			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
		}
	}
}
