// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package readeck

import (
	"encoding/json"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"miniflux.app/v2/internal/config"
)

func TestCreateBookmark(t *testing.T) {
	configureIntegrationAllowPrivateNetworksOption(t)

	entryURL := "https://example.com/article"
	entryTitle := "Example Title"
	entryContent := "<p>Some HTML content</p>"
	labels := "tag1,tag2"

	tests := []struct {
		name           string
		onlyURL        bool
		baseURL        string
		apiKey         string
		labels         string
		entryURL       string
		entryTitle     string
		entryContent   string
		serverResponse func(w http.ResponseWriter, r *http.Request)
		wantErr        bool
		errContains    string
	}{
		{
			name:         "successful bookmark creation with only URL",
			onlyURL:      true,
			labels:       labels,
			entryURL:     entryURL,
			entryTitle:   entryTitle,
			entryContent: entryContent,
			serverResponse: func(w http.ResponseWriter, r *http.Request) {
				if r.Method != http.MethodPost {
					t.Errorf("expected POST, got %s", r.Method)
				}
				if r.URL.Path != "/api/bookmarks/" {
					t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
				}
				if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
					t.Errorf("expected Authorization Bearer header, got %q", got)
				}
				if ct := r.Header.Get("Content-Type"); ct != "application/json" {
					t.Errorf("expected Content-Type application/json, got %s", ct)
				}

				body, _ := io.ReadAll(r.Body)
				var payload map[string]any
				if err := json.Unmarshal(body, &payload); err != nil {
					t.Fatalf("failed to parse JSON body: %v", err)
				}
				if u := payload["url"]; u != entryURL {
					t.Errorf("expected url %s, got %v", entryURL, u)
				}
				if title := payload["title"]; title != entryTitle {
					t.Errorf("expected title %s, got %v", entryTitle, title)
				}
				// Labels should be split into an array
				if raw := payload["labels"]; raw == nil {
					t.Errorf("expected labels to be set")
				} else if arr, ok := raw.([]any); ok {
					if len(arr) != 2 || arr[0] != "tag1" || arr[1] != "tag2" {
						t.Errorf("unexpected labels: %#v", arr)
					}
				} else {
					t.Errorf("labels should be an array, got %T", raw)
				}
				w.WriteHeader(http.StatusOK)
			},
		},
		{
			name:         "successful bookmark creation with content (multipart)",
			onlyURL:      false,
			labels:       labels,
			entryURL:     entryURL,
			entryTitle:   entryTitle,
			entryContent: entryContent,
			serverResponse: func(w http.ResponseWriter, r *http.Request) {
				if r.Method != http.MethodPost {
					t.Errorf("expected POST, got %s", r.Method)
				}
				if r.URL.Path != "/api/bookmarks/" {
					t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
				}
				if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
					t.Errorf("expected Authorization Bearer header, got %q", got)
				}
				ct := r.Header.Get("Content-Type")
				if !strings.HasPrefix(ct, "multipart/form-data;") {
					t.Errorf("expected multipart/form-data, got %s", ct)
				}
				_, after, ok := strings.Cut(ct, "boundary=")
				if !ok {
					t.Fatalf("missing multipart boundary in Content-Type: %s", ct)
				}
				boundary := after
				mr := multipart.NewReader(r.Body, boundary)

				seenLabels := []string{}
				var seenURL, seenTitle, seenFeature string
				var resourceHeader map[string]any
				var resourceBody string

				for {
					part, err := mr.NextPart()
					if err == io.EOF {
						break
					}
					if err != nil {
						t.Fatalf("reading multipart: %v", err)
					}
					name := part.FormName()
					data, _ := io.ReadAll(part)
					switch name {
					case "url":
						seenURL = string(data)
					case "title":
						seenTitle = string(data)
					case "feature_find_main":
						seenFeature = string(data)
					case "labels":
						seenLabels = append(seenLabels, string(data))
					case "resource":
						// First line is JSON header, then newline, then content
						all := string(data)
						before, after, ok := strings.Cut(all, "\n")
						if !ok {
							t.Fatalf("resource content missing header separator")
						}
						headerJSON := before
						resourceBody = after
						if err := json.Unmarshal([]byte(headerJSON), &resourceHeader); err != nil {
							t.Fatalf("invalid resource header JSON: %v", err)
						}
					}
				}

				if seenURL != entryURL {
					t.Errorf("expected url %s, got %s", entryURL, seenURL)
				}
				if seenTitle != entryTitle {
					t.Errorf("expected title %s, got %s", entryTitle, seenTitle)
				}
				if seenFeature != "false" {
					t.Errorf("expected feature_find_main to be 'false', got %s", seenFeature)
				}
				if len(seenLabels) != 2 || seenLabels[0] != "tag1" || seenLabels[1] != "tag2" {
					t.Errorf("unexpected labels: %#v", seenLabels)
				}
				if resourceHeader == nil {
					t.Fatalf("missing resource header")
				}
				if hURL, _ := resourceHeader["url"].(string); hURL != entryURL {
					t.Errorf("expected resource header url %s, got %v", entryURL, hURL)
				}
				if headers, ok := resourceHeader["headers"].(map[string]any); ok {
					if ct, _ := headers["content-type"].(string); ct != "text/html; charset=utf-8" {
						t.Errorf("expected resource header content-type text/html; charset=utf-8, got %v", ct)
					}
				} else {
					t.Errorf("missing resource header 'headers' field")
				}
				if resourceBody != entryContent {
					t.Errorf("expected resource body %q, got %q", entryContent, resourceBody)
				}

				w.WriteHeader(http.StatusOK)
			},
		},
		{
			name:         "error when server returns 400",
			onlyURL:      true,
			labels:       labels,
			entryURL:     entryURL,
			entryTitle:   entryTitle,
			entryContent: entryContent,
			serverResponse: func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusBadRequest)
			},
			wantErr:     true,
			errContains: "unable to create bookmark",
		},
		{
			name:           "error when missing baseURL or apiKey",
			onlyURL:        true,
			baseURL:        "",
			apiKey:         "",
			labels:         labels,
			entryURL:       entryURL,
			entryTitle:     entryTitle,
			entryContent:   entryContent,
			serverResponse: nil,
			wantErr:        true,
			errContains:    "missing base URL or API key",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var serverURL string
			if tt.serverResponse != nil {
				srv := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
				defer srv.Close()
				serverURL = srv.URL
			}
			baseURL := tt.baseURL
			if baseURL == "" {
				baseURL = serverURL
			}
			apiKey := tt.apiKey
			if apiKey == "" {
				apiKey = "test-api-key"
			}

			client := NewClient(baseURL, apiKey, tt.labels, tt.onlyURL)
			err := client.CreateBookmark(tt.entryURL, tt.entryTitle, tt.entryContent)

			if tt.wantErr {
				if err == nil {
					t.Fatalf("expected error, got none")
				}
				if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
					t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error())
				}
			} else if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}

func TestNewClient(t *testing.T) {
	baseURL := "https://readeck.example.com"
	apiKey := "key"
	labels := "tag1,tag2"
	onlyURL := true

	c := NewClient(baseURL, apiKey, labels, onlyURL)
	if c.baseURL != baseURL {
		t.Errorf("expected baseURL %s, got %s", baseURL, c.baseURL)
	}
	if c.apiKey != apiKey {
		t.Errorf("expected apiKey %s, got %s", apiKey, c.apiKey)
	}
	if c.labels != labels {
		t.Errorf("expected labels %s, got %s", labels, c.labels)
	}
	if c.onlyURL != onlyURL {
		t.Errorf("expected onlyURL %v, got %v", onlyURL, c.onlyURL)
	}
}

func configureIntegrationAllowPrivateNetworksOption(t *testing.T) {
	t.Helper()

	t.Setenv("INTEGRATION_ALLOW_PRIVATE_NETWORKS", "1")

	configParser := config.NewConfigParser()
	parsedOptions, err := configParser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf("Unable to configure test options: %v", err)
	}

	previousOptions := config.Opts
	config.Opts = parsedOptions
	t.Cleanup(func() {
		config.Opts = previousOptions
	})
}
