// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build unix

package ipnlocal

import (
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"testing"
	"time"

	"tailscale.com/tstest"
)

func TestExpandProxyArgUnix(t *testing.T) {
	tests := []struct {
		input        string
		wantURL      string
		wantInsecure bool
	}{
		{
			input:   "unix:/tmp/test.sock",
			wantURL: "unix:/tmp/test.sock",
		},
		{
			input:   "unix:/var/run/docker.sock",
			wantURL: "unix:/var/run/docker.sock",
		},
		{
			input:   "unix:./relative.sock",
			wantURL: "unix:./relative.sock",
		},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			gotURL, gotInsecure := expandProxyArg(tt.input)
			if gotURL != tt.wantURL {
				t.Errorf("expandProxyArg(%q) url = %q, want %q", tt.input, gotURL, tt.wantURL)
			}
			if gotInsecure != tt.wantInsecure {
				t.Errorf("expandProxyArg(%q) insecure = %v, want %v", tt.input, gotInsecure, tt.wantInsecure)
			}
		})
	}
}

func TestServeUnixSocket(t *testing.T) {
	// Create a temporary directory for our socket
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "test.sock")

	// Create a test HTTP server on Unix socket
	listener, err := net.Listen("unix", socketPath)
	if err != nil {
		t.Fatalf("failed to create unix socket listener: %v", err)
	}
	defer listener.Close()

	testResponse := "Hello from Unix socket!"
	testServer := &http.Server{
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "text/plain")
			fmt.Fprint(w, testResponse)
		}),
	}

	go testServer.Serve(listener)
	defer testServer.Close()

	// Wait for server to be ready
	time.Sleep(50 * time.Millisecond)

	// Create LocalBackend with test logger
	logf := tstest.WhileTestRunningLogger(t)
	b := newTestBackend(t)
	b.logf = logf

	// Test creating proxy handler for Unix socket
	handler, err := b.proxyHandlerForBackend("unix:" + socketPath)
	if err != nil {
		t.Fatalf("proxyHandlerForBackend failed: %v", err)
	}

	// Verify it's a reverseProxy with correct socketPath
	rp, ok := handler.(*reverseProxy)
	if !ok {
		t.Fatalf("expected *reverseProxy, got %T", handler)
	}
	if rp.socketPath != socketPath {
		t.Errorf("socketPath = %q, want %q", rp.socketPath, socketPath)
	}
	if rp.url.Host != "localhost" {
		t.Errorf("url.Host = %q, want %q", rp.url.Host, "localhost")
	}
}

func TestServeUnixSocketErrors(t *testing.T) {
	logf := tstest.WhileTestRunningLogger(t)
	b := newTestBackend(t)
	b.logf = logf

	// Test empty socket path
	_, err := b.proxyHandlerForBackend("unix:")
	if err == nil {
		t.Error("expected error for empty socket path")
	}

	// Test non-existent socket - should create handler but fail on request
	nonExistentSocket := filepath.Join(t.TempDir(), "nonexistent.sock")
	handler, err := b.proxyHandlerForBackend("unix:" + nonExistentSocket)
	if err != nil {
		t.Fatalf("proxyHandlerForBackend failed: %v", err)
	}

	req := httptest.NewRequest("GET", "http://foo.test.ts.net/", nil)
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)

	// Should get a 502 Bad Gateway when socket doesn't exist
	if rec.Code != http.StatusBadGateway {
		t.Errorf("got status %d, want %d for non-existent socket", rec.Code, http.StatusBadGateway)
	}
}

func TestReverseProxyConfigurationUnix(t *testing.T) {
	b := newTestBackend(t)

	// Test that Unix socket backend creates proper reverseProxy
	backend := "unix:/var/run/test.sock"
	handler, err := b.proxyHandlerForBackend(backend)
	if err != nil {
		t.Fatalf("proxyHandlerForBackend failed: %v", err)
	}

	rp, ok := handler.(*reverseProxy)
	if !ok {
		t.Fatalf("expected *reverseProxy, got %T", handler)
	}

	// Verify configuration
	if rp.socketPath != "/var/run/test.sock" {
		t.Errorf("socketPath = %q, want %q", rp.socketPath, "/var/run/test.sock")
	}
	if rp.backend != backend {
		t.Errorf("backend = %q, want %q", rp.backend, backend)
	}
	if rp.insecure {
		t.Error("insecure should be false for unix sockets")
	}
	expectedURL := url.URL{Scheme: "http", Host: "localhost"}
	if rp.url.Scheme != expectedURL.Scheme || rp.url.Host != expectedURL.Host {
		t.Errorf("url = %v, want %v", rp.url, expectedURL)
	}
}

func TestServeBlocksTailscaledSocket(t *testing.T) {
	// Use /tmp to avoid macOS socket path length limits
	tmpDir, err := os.MkdirTemp("/tmp", "ts-test-*")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	tailscaledSocket := filepath.Join(tmpDir, "ts.sock")

	// Create actual socket file
	listener, err := net.Listen("unix", tailscaledSocket)
	if err != nil {
		t.Fatalf("failed to create tailscaled socket: %v", err)
	}
	defer listener.Close()

	b := newTestBackend(t)
	b.sys.SocketPath = tailscaledSocket

	// Direct path to tailscaled socket should be blocked
	_, err = b.proxyHandlerForBackend("unix:" + tailscaledSocket)
	if !errors.Is(err, ErrProxyToTailscaledSocket) {
		t.Errorf("direct path: got err=%v, want ErrProxyToTailscaledSocket", err)
	}

	// Symlink to tailscaled socket should be blocked
	symlinkPath := filepath.Join(tmpDir, "link")
	if err := os.Symlink(tailscaledSocket, symlinkPath); err != nil {
		t.Fatalf("failed to create symlink: %v", err)
	}

	_, err = b.proxyHandlerForBackend("unix:" + symlinkPath)
	if !errors.Is(err, ErrProxyToTailscaledSocket) {
		t.Errorf("symlink: got err=%v, want ErrProxyToTailscaledSocket", err)
	}

	// Different socket should work
	otherSocket := filepath.Join(tmpDir, "ok.sock")
	listener2, err := net.Listen("unix", otherSocket)
	if err != nil {
		t.Fatalf("failed to create other socket: %v", err)
	}
	defer listener2.Close()

	handler, err := b.proxyHandlerForBackend("unix:" + otherSocket)
	if err != nil {
		t.Errorf("legitimate socket should not be blocked: %v", err)
	}
	if handler == nil {
		t.Error("expected valid handler for legitimate socket")
	}
}
