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

package cooked

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
)

func TestReadShoppingListACIDAuthenticationLogin3StoresCookies(t *testing.T) {
	var sawSessionCookie bool

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/public/login":
			if r.Method != http.MethodPost {
				t.Fatalf("login method = %s, want POST", r.Method)
			}

			var request loginRequest
			if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
				t.Fatalf("decode login request: %v", err)
			}
			if request.Username != "configured-user" || request.Password != "configured-password" {
				t.Fatalf("login request = %#v, want configured credentials", request)
			}

			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
			writeJSON(t, w, loginResponse{Username: "returned-user"})
		case "/api/user/returned-user/shopping-list":
			cookie, err := r.Cookie("cooked_session")
			if err != nil {
				t.Fatalf("missing session cookie: %v", err)
			}
			if cookie.Value != "session-value" {
				t.Fatalf("session cookie = %q, want session-value", cookie.Value)
			}
			sawSessionCookie = true
			writeJSON(t, w, shoppingListResponse{
				ShoppingList: ShoppingList{
					Aisles: []Aisle{{
						ID:   "pantry",
						Name: "Pantry",
						ProductGroups: []ProductGroup{{
							ID:       "pasta",
							Name:     "Pasta",
							Quantity: "200g",
						}},
					}},
				},
				Recipes: []string{"recipe-id"},
			})
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	baseURL, err := url.Parse(server.URL)
	if err != nil {
		t.Fatalf("parse server URL: %v", err)
	}
	client, err := NewClient(baseURL, "configured-user", "configured-password")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	shoppingList, err := client.ReadShoppingList(context.Background())
	if err != nil {
		t.Fatalf("ReadShoppingList() error = %v", err)
	}

	if !sawSessionCookie {
		t.Fatal("shopping-list request did not receive session cookie")
	}
	if len(shoppingList.Aisles) != 1 || shoppingList.Aisles[0].ProductGroups[0].Name != "Pasta" {
		t.Fatalf("shopping list = %#v, want one Pasta item", shoppingList)
	}
}

func TestListRecipesACIDRecipesRead1ListsSavedRecipes(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/public/login":
			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
			writeJSON(t, w, loginResponse{Username: "returned-user"})
		case "/api/user/returned-user/recipes":
			if got := r.URL.Query().Get("page"); got != "2" {
				t.Fatalf("page query = %q, want 2", got)
			}
			if got := r.URL.Query().Get("page-count"); got != "5" {
				t.Fatalf("page-count query = %q, want 5", got)
			}
			writeJSON(t, w, recipeListResponse{
				Recipes: []RecipeCard{{
					ID:           "recipe-1",
					Title:        "Pasta",
					ThumbnailURL: "https://example.invalid/thumb.jpg",
				}},
			})
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	baseURL, err := url.Parse(server.URL)
	if err != nil {
		t.Fatalf("parse server URL: %v", err)
	}
	client, err := NewClient(baseURL, "configured-user", "configured-password")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	recipes, err := client.ListRecipes(context.Background(), 2, 5)
	if err != nil {
		t.Fatalf("ListRecipes() error = %v", err)
	}

	if len(recipes) != 1 {
		t.Fatalf("recipes length = %d, want 1", len(recipes))
	}
	if recipes[0].ID != "recipe-1" || recipes[0].Title != "Pasta" {
		t.Fatalf("recipe = %#v, want recipe-1 Pasta", recipes[0])
	}
}

func TestSearchRecipesACIDRecipesRead2SearchesSavedRecipes(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/public/login":
			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
			writeJSON(t, w, loginResponse{Username: "returned-user"})
		case "/api/user/returned-user/recipes/search":
			if r.Method != http.MethodGet {
				t.Fatalf("search method = %s, want GET", r.Method)
			}
			if got := r.URL.Query().Get("q"); got != "pasta & tomato" {
				t.Fatalf("q query = %q, want pasta & tomato", got)
			}
			if got := r.URL.Query().Get("page"); got != "3" {
				t.Fatalf("page query = %q, want 3", got)
			}
			writeJSON(t, w, recipeListResponse{
				Recipes: []RecipeCard{{
					ID:           "recipe-1",
					Title:        "Pasta",
					ThumbnailURL: "https://example.invalid/thumb.jpg",
				}},
			})
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	baseURL, err := url.Parse(server.URL)
	if err != nil {
		t.Fatalf("parse server URL: %v", err)
	}
	client, err := NewClient(baseURL, "configured-user", "configured-password")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	recipes, err := client.SearchRecipes(context.Background(), "pasta & tomato", 3)
	if err != nil {
		t.Fatalf("SearchRecipes() error = %v", err)
	}

	if len(recipes) != 1 {
		t.Fatalf("recipes length = %d, want 1", len(recipes))
	}
	if recipes[0].ID != "recipe-1" || recipes[0].Title != "Pasta" {
		t.Fatalf("recipe = %#v, want recipe-1 Pasta", recipes[0])
	}
}

func TestReadRecipeMetadataACIDRecipesRead6ReadsMetadata(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.EscapedPath() {
		case "/api/public/login":
			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
			writeJSON(t, w, loginResponse{Username: "returned-user"})
		case "/api/recipe/recipe%2Fwith%20space/metadata":
			if r.Method != http.MethodGet {
				t.Fatalf("metadata method = %s, want GET", r.Method)
			}
			cookie, err := r.Cookie("cooked_session")
			if err != nil {
				t.Fatalf("missing session cookie: %v", err)
			}
			if cookie.Value != "session-value" {
				t.Fatalf("session cookie = %q, want session-value", cookie.Value)
			}

			w.Header().Set("Content-Type", "application/json")
			if _, err := w.Write([]byte(`{
				"title": "Pasta with Tomato Sauce",
				"image-urls": ["https://example.invalid/pasta.jpg"],
				"owner": "returned-user",
				"edit-permission": true
			}`)); err != nil {
				t.Fatalf("write metadata response: %v", err)
			}
		default:
			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
		}
	}))
	defer server.Close()

	baseURL, err := url.Parse(server.URL)
	if err != nil {
		t.Fatalf("parse server URL: %v", err)
	}
	client, err := NewClient(baseURL, "configured-user", "configured-password")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	metadata, err := client.ReadRecipeMetadata(context.Background(), "recipe/with space")
	if err != nil {
		t.Fatalf("ReadRecipeMetadata() error = %v", err)
	}

	if metadata.Title != "Pasta with Tomato Sauce" {
		t.Fatalf("metadata title = %q, want Pasta with Tomato Sauce", metadata.Title)
	}
	if len(metadata.ImageURLs) != 1 || metadata.ImageURLs[0] != "https://example.invalid/pasta.jpg" {
		t.Fatalf("metadata image URLs = %#v, want one pasta image", metadata.ImageURLs)
	}
	if metadata.Owner != "returned-user" || !metadata.EditPermission {
		t.Fatalf("metadata owner/edit = %q/%v, want returned-user/true", metadata.Owner, metadata.EditPermission)
	}
}

func TestReadRecipeContentACIDRecipesRead7And8ReadsContentAndPortions(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.EscapedPath() {
		case "/api/public/login":
			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
			writeJSON(t, w, loginResponse{Username: "returned-user"})
		case "/api/recipe/recipe%2Fwith%20space/content":
			if r.Method != http.MethodGet {
				t.Fatalf("content method = %s, want GET", r.Method)
			}
			cookie, err := r.Cookie("cooked_session")
			if err != nil {
				t.Fatalf("missing session cookie: %v", err)
			}
			if cookie.Value != "session-value" {
				t.Fatalf("session cookie = %q, want session-value", cookie.Value)
			}

			writeJSON(t, w, RecipeContent{
				Content:  "# Pasta\n\n- 200g pasta\n\n1. Boil pasta.",
				Portions: 2,
			})
		default:
			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
		}
	}))
	defer server.Close()

	baseURL, err := url.Parse(server.URL)
	if err != nil {
		t.Fatalf("parse server URL: %v", err)
	}
	client, err := NewClient(baseURL, "configured-user", "configured-password")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	content, err := client.ReadRecipeContent(context.Background(), "recipe/with space")
	if err != nil {
		t.Fatalf("ReadRecipeContent() error = %v", err)
	}

	if content.Content != "# Pasta\n\n- 200g pasta\n\n1. Boil pasta." {
		t.Fatalf("content = %q, want recipe markdown", content.Content)
	}
	if content.Portions != 2 {
		t.Fatalf("portions = %d, want 2", content.Portions)
	}
}

func TestUserPathEscapesAuthenticatedUsername(t *testing.T) {
	client := &Client{authenticatedUsername: "user/name"}

	got := client.userPath("/api/user/{username}/shopping-list")
	if got != "/api/user/user%2Fname/shopping-list" {
		t.Fatalf("userPath() = %q, want escaped username", got)
	}
}

func TestNewRequestPreservesEscapedUserPathWithQuery(t *testing.T) {
	baseURL, err := url.Parse("https://example.invalid")
	if err != nil {
		t.Fatalf("parse base URL: %v", err)
	}
	client := &Client{baseURL: baseURL}

	request, err := client.newRequest(context.Background(), http.MethodGet, "/api/user/user%2Fname/recipes?page=2&page-count=5", nil)
	if err != nil {
		t.Fatalf("newRequest() error = %v", err)
	}

	if request.URL.String() != "https://example.invalid/api/user/user%2Fname/recipes?page=2&page-count=5" {
		t.Fatalf("request URL = %q, want escaped username preserved", request.URL.String())
	}
}

func writeJSON[T loginResponse | shoppingListResponse | recipeListResponse | RecipeContent](t *testing.T, w http.ResponseWriter, value T) {
	t.Helper()

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(value); err != nil {
		t.Fatalf("write JSON: %v", err)
	}
}
