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

package fetcher // import "miniflux.app/v2/internal/reader/fetcher"

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

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

func TestNewRequestBuilder(t *testing.T) {
	builder := NewRequestBuilder()
	if builder == nil {
		t.Fatal("NewRequestBuilder should not return nil")
	}
	if builder.clientTimeout != defaultHTTPClientTimeout {
		t.Errorf("Expected default timeout %d, got %d", defaultHTTPClientTimeout, builder.clientTimeout)
	}
	if builder.headers == nil {
		t.Fatal("Headers should be initialized")
	}
}

func TestRequestBuilder_WithHeader(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Custom-Header") != "custom-value" {
			t.Errorf("Expected Custom-Header to be 'custom-value', got '%s'", r.Header.Get("Custom-Header"))
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.WithHeader("Custom-Header", "custom-value").ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	defer resp.Body.Close()
}

func TestRequestBuilder_WithETag(t *testing.T) {
	tests := []struct {
		name     string
		etag     string
		expected string
	}{
		{"with etag", "test-etag", "test-etag"},
		{"empty etag", "", ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Header.Get("If-None-Match") != tt.expected {
					t.Errorf("Expected If-None-Match to be '%s', got '%s'", tt.expected, r.Header.Get("If-None-Match"))
				}
				w.WriteHeader(http.StatusOK)
			}))
			defer server.Close()

			builder := NewRequestBuilder()
			resp, err := builder.WithETag(tt.etag).ExecuteRequest(server.URL)
			if err != nil {
				t.Fatalf("Expected no error, got %v", err)
			}
			defer resp.Body.Close()
		})
	}
}

func TestRequestBuilder_WithLastModified(t *testing.T) {
	tests := []struct {
		name         string
		lastModified string
		expected     string
	}{
		{"with last modified", "Mon, 02 Jan 2006 15:04:05 GMT", "Mon, 02 Jan 2006 15:04:05 GMT"},
		{"empty last modified", "", ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Header.Get("If-Modified-Since") != tt.expected {
					t.Errorf("Expected If-Modified-Since to be '%s', got '%s'", tt.expected, r.Header.Get("If-Modified-Since"))
				}
				w.WriteHeader(http.StatusOK)
			}))
			defer server.Close()

			builder := NewRequestBuilder()
			resp, err := builder.WithLastModified(tt.lastModified).ExecuteRequest(server.URL)
			if err != nil {
				t.Fatalf("Expected no error, got %v", err)
			}
			defer resp.Body.Close()
		})
	}
}

func TestRequestBuilder_WithUserAgent(t *testing.T) {
	tests := []struct {
		name           string
		userAgent      string
		defaultAgent   string
		expectedHeader string
	}{
		{"custom user agent", "CustomAgent/1.0", "DefaultAgent/1.0", "CustomAgent/1.0"},
		{"default user agent", "", "DefaultAgent/1.0", "DefaultAgent/1.0"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Header.Get("User-Agent") != tt.expectedHeader {
					t.Errorf("Expected User-Agent to be '%s', got '%s'", tt.expectedHeader, r.Header.Get("User-Agent"))
				}
				w.WriteHeader(http.StatusOK)
			}))
			defer server.Close()

			builder := NewRequestBuilder()
			resp, err := builder.WithUserAgent(tt.userAgent, tt.defaultAgent).ExecuteRequest(server.URL)
			if err != nil {
				t.Fatalf("Expected no error, got %v", err)
			}
			defer resp.Body.Close()
		})
	}
}

func TestRequestBuilder_WithCookie(t *testing.T) {
	tests := []struct {
		name     string
		cookie   string
		expected string
	}{
		{"with cookie", "session=abc123; lang=en", "session=abc123; lang=en"},
		{"empty cookie", "", ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Header.Get("Cookie") != tt.expected {
					t.Errorf("Expected Cookie to be '%s', got '%s'", tt.expected, r.Header.Get("Cookie"))
				}
				w.WriteHeader(http.StatusOK)
			}))
			defer server.Close()

			builder := NewRequestBuilder()
			resp, err := builder.WithCookie(tt.cookie).ExecuteRequest(server.URL)
			if err != nil {
				t.Fatalf("Expected no error, got %v", err)
			}
			defer resp.Body.Close()
		})
	}
}

func TestRequestBuilder_WithUsernameAndPassword(t *testing.T) {
	tests := []struct {
		name     string
		username string
		password string
		expected string
	}{
		{"with credentials", "test", "password", "Basic dGVzdDpwYXNzd29yZA=="}, // base64 of "test:password"
		{"empty username", "", "password", ""},
		{"empty password", "test", "", ""},
		{"both empty", "", "", ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Header.Get("Authorization") != tt.expected {
					t.Errorf("Expected Authorization to be '%s', got '%s'", tt.expected, r.Header.Get("Authorization"))
				}
				w.WriteHeader(http.StatusOK)
			}))
			defer server.Close()

			builder := NewRequestBuilder()
			resp, err := builder.WithUsernameAndPassword(tt.username, tt.password).ExecuteRequest(server.URL)
			if err != nil {
				t.Fatalf("Expected no error, got %v", err)
			}
			defer resp.Body.Close()
		})
	}
}

func TestRequestBuilder_DefaultAcceptHeader(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Accept") != defaultAcceptHeader {
			t.Errorf("Expected Accept to be '%s', got '%s'", defaultAcceptHeader, r.Header.Get("Accept"))
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	defer resp.Body.Close()
}

func TestRequestBuilder_CustomAcceptHeaderNotOverridden(t *testing.T) {
	customAccept := "application/json"
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Accept") != customAccept {
			t.Errorf("Expected Accept to be '%s', got '%s'", customAccept, r.Header.Get("Accept"))
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.WithHeader("Accept", customAccept).ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	defer resp.Body.Close()
}

func TestRequestBuilder_WithTimeout(t *testing.T) {
	builder := NewRequestBuilder()
	builder = builder.WithTimeout(30 * time.Second)

	if builder.clientTimeout != 30*time.Second {
		t.Errorf("Expected timeout to be 30, got %d", builder.clientTimeout)
	}
}

func TestRequestBuilder_WithoutRedirects(t *testing.T) {
	// Create a redirect server
	redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer redirectServer.Close()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, redirectServer.URL, http.StatusFound)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.WithoutRedirects().ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusFound {
		t.Errorf("Expected status code %d, got %d", http.StatusFound, resp.StatusCode)
	}
}

func TestRequestBuilder_Clone(t *testing.T) {
	original := NewRequestBuilder().WithHeader("X-Shared", "value")

	clone := original.Clone().WithoutRedirects()
	clone.WithHeader("X-Clone-Only", "value")

	if original.withoutRedirects {
		t.Error("Mutating the clone should not disable redirects on the original")
	}

	if original.headers.Get("X-Clone-Only") != "" {
		t.Error("Mutating the clone's headers should not affect the original")
	}

	if clone.headers.Get("X-Shared") != "value" {
		t.Error("Expected the clone to inherit the original headers")
	}

	if clone.clientTimeout != original.clientTimeout {
		t.Error("Expected the clone to inherit the original timeout")
	}
}

func TestRequestBuilder_DisableHTTP2(t *testing.T) {
	builder := NewRequestBuilder()
	builder = builder.DisableHTTP2(true)

	if !builder.disableHTTP2 {
		t.Error("Expected disableHTTP2 to be true")
	}
}

func TestRequestBuilder_IgnoreTLSErrors(t *testing.T) {
	builder := NewRequestBuilder()
	builder = builder.IgnoreTLSErrors(true)

	if !builder.ignoreTLSErrors {
		t.Error("Expected ignoreTLSErrors to be true")
	}
}

func TestRequestBuilder_WithoutCompression(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Accept-Encoding") != "identity" {
			t.Errorf("Expected Accept-Encoding to be 'identity', got '%s'", r.Header.Get("Accept-Encoding"))
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.WithoutCompression().ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	defer resp.Body.Close()
}

func TestRequestBuilder_WithCompression(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Accept-Encoding") != "br,gzip" {
			t.Errorf("Expected Accept-Encoding to be 'br,gzip', got '%s'", r.Header.Get("Accept-Encoding"))
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	defer resp.Body.Close()
}

func TestRequestBuilder_ConnectionCloseHeader(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Connection") != "close" {
			t.Errorf("Expected Connection to be 'close', got '%s'", r.Header.Get("Connection"))
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	defer resp.Body.Close()
}

func TestRequestBuilder_WithCustomApplicationProxyURL(t *testing.T) {
	proxyURL, _ := url.Parse("http://proxy.example.com:8080")
	builder := NewRequestBuilder()
	builder = builder.WithCustomApplicationProxyURL(proxyURL)

	if builder.clientProxyURL != proxyURL {
		t.Error("Expected clientProxyURL to be set")
	}
}

func TestRequestBuilder_UseCustomApplicationProxyURL(t *testing.T) {
	builder := NewRequestBuilder()
	builder = builder.UseCustomApplicationProxyURL(true)

	if !builder.useClientProxy {
		t.Error("Expected useClientProxy to be true")
	}
}

func TestRequestBuilder_WithCustomFeedProxyURL(t *testing.T) {
	proxyURL := "http://feed-proxy.example.com:8080"
	builder := NewRequestBuilder()
	builder = builder.WithCustomFeedProxyURL(proxyURL)

	if builder.feedProxyURL != proxyURL {
		t.Errorf("Expected feedProxyURL to be '%s', got '%s'", proxyURL, builder.feedProxyURL)
	}
}

func TestRequestBuilder_ChainedMethods(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check multiple headers
		if r.Header.Get("User-Agent") != "TestAgent/1.0" {
			t.Errorf("Expected User-Agent to be 'TestAgent/1.0', got '%s'", r.Header.Get("User-Agent"))
		}
		if r.Header.Get("Cookie") != "test=value" {
			t.Errorf("Expected Cookie to be 'test=value', got '%s'", r.Header.Get("Cookie"))
		}
		if r.Header.Get("If-None-Match") != "etag123" {
			t.Errorf("Expected If-None-Match to be 'etag123', got '%s'", r.Header.Get("If-None-Match"))
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.
		WithUserAgent("TestAgent/1.0", "DefaultAgent/1.0").
		WithCookie("test=value").
		WithETag("etag123").
		WithTimeout(10 * time.Second).
		ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	defer resp.Body.Close()
}

func TestRequestBuilder_InvalidURL(t *testing.T) {
	builder := NewRequestBuilder()
	_, err := builder.ExecuteRequest("invalid-url")
	if err == nil {
		t.Error("Expected error for invalid URL")
	}
}

func TestRequestBuilder_RefusePrivateNetworkByDefault(t *testing.T) {
	configureFetcherAllowPrivateNetworksOption(t, "0")

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	_, err := builder.ExecuteRequest(server.URL)
	if err == nil {
		t.Fatal("Expected private network request to be rejected")
	}

	if !strings.Contains(err.Error(), "refusing to access private network host") {
		t.Fatalf("Unexpected error for private network request: %v", err)
	}
}

func TestRequestBuilder_AllowPrivateNetworkWhenEnabled(t *testing.T) {
	configureFetcherAllowPrivateNetworksOption(t, "1")

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	resp, err := builder.ExecuteRequest(server.URL)
	if err != nil {
		t.Fatalf("Expected private network request to succeed when enabled: %v", err)
	}
	defer resp.Body.Close()
}

func TestRequestBuilder_AllowPrivateConfiguredProxy(t *testing.T) {
	configureFetcherAllowPrivateNetworksOption(t, "0")

	tests := []struct {
		name      string
		configure func(t *testing.T, builder *RequestBuilder, proxyURL string) *RequestBuilder
	}{
		{
			name: "feed proxy",
			configure: func(t *testing.T, builder *RequestBuilder, proxyURL string) *RequestBuilder {
				return builder.WithCustomFeedProxyURL(proxyURL)
			},
		},
		{
			name: "application proxy",
			configure: func(t *testing.T, builder *RequestBuilder, proxyURL string) *RequestBuilder {
				t.Helper()

				parsedProxyURL, err := url.Parse(proxyURL)
				if err != nil {
					t.Fatalf("Unable to parse proxy URL: %v", err)
				}

				return builder.WithCustomApplicationProxyURL(parsedProxyURL).UseCustomApplicationProxyURL(true)
			},
		},
		{
			name: "proxy rotator",
			configure: func(t *testing.T, builder *RequestBuilder, proxyURL string) *RequestBuilder {
				t.Helper()

				rotator, err := proxyrotator.NewProxyRotator([]string{proxyURL})
				if err != nil {
					t.Fatalf("Unable to create proxy rotator: %v", err)
				}

				return builder.WithProxyRotator(rotator)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			targetURL := "http://feed.invalid/rss.xml"
			proxyRequests := make(chan string, 1)
			proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				select {
				case proxyRequests <- r.URL.String():
				default:
				}

				w.WriteHeader(http.StatusOK)
			}))
			defer proxyServer.Close()

			builder := tt.configure(t, NewRequestBuilder(), proxyServer.URL)
			resp, err := builder.ExecuteRequest(targetURL)
			if err != nil {
				t.Fatalf("Expected private proxy request to succeed: %v", err)
			}
			defer resp.Body.Close()

			select {
			case gotURL := <-proxyRequests:
				if gotURL != targetURL {
					t.Fatalf("Expected proxy request URL to be %q, got %q", targetURL, gotURL)
				}
			default:
				t.Fatal("Expected request to be sent through the proxy")
			}
		})
	}
}

func TestRequestBuilder_RefusePrivateNetworkOnRedirect(t *testing.T) {
	configureFetcherAllowPrivateNetworksOption(t, "0")

	// Target server on a loopback address (private).
	privateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer privateServer.Close()

	// Redirector that sends the client to the private server.
	// Because the Control callback checks the IP at connection time, the
	// redirect target is also validated (unlike a pre-flight DNS check).
	redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, privateServer.URL, http.StatusFound)
	}))
	defer redirectServer.Close()

	builder := NewRequestBuilder()
	_, err := builder.ExecuteRequest(redirectServer.URL)
	if err == nil {
		t.Fatal("Expected redirect to private network to be rejected")
	}

	if !strings.Contains(err.Error(), "refusing to access private network host") {
		t.Fatalf("Unexpected error for redirected private network request: %v", err)
	}
}

func TestRequestBuilder_TimeoutConfiguration(t *testing.T) {
	// Create a slow server that blocks until the client disconnects, so
	// server.Close() does not have to wait for a fixed sleep to elapse.
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		<-r.Context().Done()
	}))
	defer server.Close()

	builder := NewRequestBuilder()
	start := time.Now()
	_, err := builder.WithTimeout(100 * time.Millisecond).ExecuteRequest(server.URL)
	duration := time.Since(start)

	if err == nil {
		t.Error("Expected timeout error")
	}

	// Should timeout around 100ms, allow some margin
	if duration > 500*time.Millisecond {
		t.Errorf("Expected timeout around 100ms, took %v", duration)
	}
}

func configureFetcherAllowPrivateNetworksOption(t *testing.T, value string) {
	t.Helper()

	t.Setenv("FETCHER_ALLOW_PRIVATE_NETWORKS", value)

	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
	})
}
