package hostproxy

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/schmitthub/clawker/internal/logger"
)

// getFreePort returns an available TCP port on localhost.
func getFreePort(t *testing.T) int {
	t.Helper()
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("failed to get free port: %v", err)
	}
	port := l.Addr().(*net.TCPAddr).Port
	l.Close()
	return port
}

func TestServerHealthEndpoint(t *testing.T) {
	s := &Server{log: logger.Nop()}
	req := httptest.NewRequest(http.MethodGet, "/health", nil)
	w := httptest.NewRecorder()

	s.handleHealth(w, req)

	resp := w.Result()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected status 200, got %d", resp.StatusCode)
	}

	var result healthResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}

	if result.Status != "ok" {
		t.Errorf("expected status 'ok', got %q", result.Status)
	}

	if result.Service != "clawker-host-proxy" {
		t.Errorf("expected service 'clawker-host-proxy', got %q", result.Service)
	}
}

func TestServerOpenURLEndpoint(t *testing.T) {
	tests := []struct {
		name       string
		body       string
		wantStatus int
		wantError  string
	}{
		{
			name:       "invalid json",
			body:       "not json",
			wantStatus: http.StatusBadRequest,
			wantError:  "invalid JSON request body",
		},
		{
			name:       "empty url",
			body:       `{}`,
			wantStatus: http.StatusBadRequest,
			wantError:  "url field is required",
		},
		{
			name:       "empty url string",
			body:       `{"url": ""}`,
			wantStatus: http.StatusBadRequest,
			wantError:  "url field is required",
		},
		{
			name:       "file scheme rejected",
			body:       `{"url": "file:///etc/passwd"}`,
			wantStatus: http.StatusBadRequest,
			wantError:  "only http and https URLs are allowed",
		},
		{
			name:       "javascript scheme rejected",
			body:       `{"url": "javascript:alert(1)"}`,
			wantStatus: http.StatusBadRequest,
			wantError:  "only http and https URLs are allowed",
		},
		{
			name:       "data scheme rejected",
			body:       `{"url": "data:text/html,<script>alert(1)</script>"}`,
			wantStatus: http.StatusBadRequest,
			wantError:  "only http and https URLs are allowed",
		},
		{
			name:       "relative url rejected",
			body:       `{"url": "/etc/passwd"}`,
			wantStatus: http.StatusBadRequest,
			wantError:  "only http and https URLs are allowed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &Server{log: logger.Nop()}
			req := httptest.NewRequest(http.MethodPost, "/open/url", bytes.NewBufferString(tt.body))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()

			s.handleOpenURL(w, req)

			resp := w.Result()
			if resp.StatusCode != tt.wantStatus {
				t.Errorf("expected status %d, got %d", tt.wantStatus, resp.StatusCode)
			}

			var result openURLResponse
			if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
				t.Fatalf("failed to decode response: %v", err)
			}

			if result.Success {
				t.Error("expected success to be false")
			}

			if result.Error != tt.wantError {
				t.Errorf("expected error %q, got %q", tt.wantError, result.Error)
			}
		})
	}
}

func TestServerOpenURL_EgressBlocking(t *testing.T) {
	// Write a minimal rules file: github.test:443:tls and docs.example.test:80:http.
	tmp := t.TempDir()
	rulesFile := filepath.Join(tmp, "egress-rules.yaml")
	rulesYAML := `rules:
  - action: allow
    dst: github.test
    port: 443
    proto: tls
  - action: allow
    dst: docs.example.test
    port: 80
    proto: http
`
	if err := os.WriteFile(rulesFile, []byte(rulesYAML), 0o644); err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		name         string
		body         string
		wantStatus   int
		wantSuccess  bool
		wantBrowser  bool
		wantErrorMsg string
	}{
		{
			name:        "allowed domain passes egress check",
			body:        `{"url":"https://github.test/schmitthub/clawker"}`,
			wantStatus:  http.StatusOK,
			wantSuccess: true,
			wantBrowser: true,
		},
		{
			name:         "blocked domain returns 403",
			body:         `{"url":"https://evil.test/exfil?data=stolen"}`,
			wantStatus:   http.StatusForbidden,
			wantSuccess:  false,
			wantBrowser:  false,
			wantErrorMsg: "blocked by egress policy",
		},
		{
			name:         "wrong proto for domain blocked",
			body:         `{"url":"http://github.test/foo"}`,
			wantStatus:   http.StatusForbidden,
			wantSuccess:  false,
			wantBrowser:  false,
			wantErrorMsg: "blocked by egress policy",
		},
		{
			name:        "http allowed domain passes",
			body:        `{"url":"http://docs.example.test/guide"}`,
			wantStatus:  http.StatusOK,
			wantSuccess: true,
			wantBrowser: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			browserCalled := false
			s := &Server{
				log:           logger.Nop(),
				rulesFilePath: rulesFile,
				browserFunc:   func(_ string) error { browserCalled = true; return nil },
			}

			req := httptest.NewRequest(http.MethodPost, "/open/url", bytes.NewBufferString(tt.body))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()

			s.handleOpenURL(w, req)

			resp := w.Result()
			if resp.StatusCode != tt.wantStatus {
				t.Errorf("status = %d, want %d", resp.StatusCode, tt.wantStatus)
			}

			var result openURLResponse
			if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
				t.Fatalf("failed to decode response: %v", err)
			}

			if result.Success != tt.wantSuccess {
				t.Errorf("success = %v, want %v", result.Success, tt.wantSuccess)
			}

			if browserCalled != tt.wantBrowser {
				t.Errorf("browserCalled = %v, want %v", browserCalled, tt.wantBrowser)
			}

			if tt.wantErrorMsg != "" && result.Error != tt.wantErrorMsg {
				t.Errorf("error = %q, want %q", result.Error, tt.wantErrorMsg)
			}
		})
	}
}

func TestServerOpenURL_NoRulesFile_SkipsCheck(t *testing.T) {
	browserCalled := false
	s := &Server{
		log:         logger.Nop(),
		browserFunc: func(_ string) error { browserCalled = true; return nil },
	}

	req := httptest.NewRequest(http.MethodPost, "/open/url",
		bytes.NewBufferString(`{"url":"https://any-domain.test/path"}`))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	s.handleOpenURL(w, req)

	resp := w.Result()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200 when no rules file, got %d", resp.StatusCode)
	}
	if !browserCalled {
		t.Error("expected browser to be called when no rules file configured")
	}
}

func TestServerOpenURL_MissingRulesFile_FailsClosed(t *testing.T) {
	s := &Server{
		log:           logger.Nop(),
		rulesFilePath: "/nonexistent/egress-rules.yaml",
		browserFunc:   func(_ string) error { t.Fatal("browser should not be called"); return nil },
	}

	req := httptest.NewRequest(http.MethodPost, "/open/url",
		bytes.NewBufferString(`{"url":"https://github.test/foo"}`))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	s.handleOpenURL(w, req)

	resp := w.Result()
	if resp.StatusCode != http.StatusForbidden {
		t.Errorf("expected 403 when rules file missing, got %d", resp.StatusCode)
	}

	var result openURLResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if result.Success {
		t.Error("expected success to be false")
	}
	if result.Error != "blocked by egress policy" {
		t.Errorf("expected 'blocked by egress policy', got %q", result.Error)
	}
}

func TestServerPort(t *testing.T) {
	s := NewServer(12345, logger.Nop(), "")
	if s.Port() != 12345 {
		t.Errorf("expected port 12345, got %d", s.Port())
	}
}

func TestServerIsRunning(t *testing.T) {
	s := NewServer(0, logger.Nop(), "")
	if s.IsRunning() {
		t.Error("expected server to not be running initially")
	}
}

func TestServerOpenURLBodySizeLimit(t *testing.T) {
	s := &Server{log: logger.Nop()}

	// Create a body larger than maxRequestBodySize (1MB)
	largeBody := make([]byte, maxRequestBodySize+1)
	for i := range largeBody {
		largeBody[i] = 'a'
	}

	req := httptest.NewRequest(http.MethodPost, "/open/url", bytes.NewReader(largeBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	s.handleOpenURL(w, req)

	resp := w.Result()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected status 400 for oversized body, got %d", resp.StatusCode)
	}

	var result openURLResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}

	if result.Success {
		t.Error("expected success to be false")
	}

	// The error message should indicate invalid JSON (because the body was truncated)
	if result.Error != "invalid JSON request body" {
		t.Errorf("expected error 'invalid JSON request body', got %q", result.Error)
	}
}

// --- Callback endpoint tests ---

func TestServerCallbackRegister(t *testing.T) {
	s := NewServer(18374, logger.Nop(), "")
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		s.Stop(ctx)
	})

	// Get free ports for tests that will actually start listeners
	port1 := getFreePort(t)
	port2 := getFreePort(t)

	tests := []struct {
		name       string
		body       string
		wantStatus int
		wantErr    bool
		checkResp  func(t *testing.T, resp callbackRegisterResponse)
	}{
		{
			name:       "valid registration",
			body:       fmt.Sprintf(`{"port": %d, "path": "/callback"}`, port1),
			wantStatus: http.StatusOK,
			wantErr:    false,
			checkResp: func(t *testing.T, resp callbackRegisterResponse) {
				if !resp.Success {
					t.Error("expected success")
				}
				if resp.SessionID == "" {
					t.Error("expected session_id")
				}
				if resp.ExpiresAt == "" {
					t.Error("expected expires_at")
				}
			},
		},
		{
			name:       "valid registration with timeout",
			body:       fmt.Sprintf(`{"port": %d, "timeout_seconds": 120}`, port2),
			wantStatus: http.StatusOK,
			wantErr:    false,
		},
		{
			name:       "invalid json",
			body:       "not json",
			wantStatus: http.StatusBadRequest,
			wantErr:    true,
		},
		{
			name:       "zero port",
			body:       `{"port": 0}`,
			wantStatus: http.StatusBadRequest,
			wantErr:    true,
		},
		{
			name:       "negative port",
			body:       `{"port": -1}`,
			wantStatus: http.StatusBadRequest,
			wantErr:    true,
		},
		{
			name:       "port too high",
			body:       `{"port": 70000}`,
			wantStatus: http.StatusBadRequest,
			wantErr:    true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, "/callback/register", bytes.NewBufferString(tt.body))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()

			s.handleCallbackRegister(w, req)

			resp := w.Result()
			if resp.StatusCode != tt.wantStatus {
				t.Errorf("expected status %d, got %d", tt.wantStatus, resp.StatusCode)
			}

			var result callbackRegisterResponse
			if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
				t.Fatalf("failed to decode response: %v", err)
			}

			if tt.wantErr && result.Success {
				t.Error("expected error response")
			}
			if !tt.wantErr && !result.Success {
				t.Errorf("expected success, got error: %s", result.Error)
			}

			if tt.checkResp != nil {
				tt.checkResp(t, result)
			}
		})
	}
}

func TestServerCallbackGetData(t *testing.T) {
	s := NewServer(18374, logger.Nop(), "")
	defer s.Stop(context.Background())

	// Register a session first
	session, _ := s.callbackChannel.Register(8080, "/callback", 5*time.Minute)

	t.Run("no callback yet", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/callback/"+session.ID+"/data", nil)
		req.SetPathValue("session", session.ID)
		w := httptest.NewRecorder()

		s.handleCallbackGetData(w, req)

		resp := w.Result()
		if resp.StatusCode != http.StatusOK {
			t.Errorf("expected status 200, got %d", resp.StatusCode)
		}

		var result callbackDataResponse
		json.NewDecoder(resp.Body).Decode(&result)
		if result.Received {
			t.Error("expected received=false")
		}
	})

	t.Run("session not found", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/callback/nonexistent/data", nil)
		req.SetPathValue("session", "nonexistent")
		w := httptest.NewRecorder()

		s.handleCallbackGetData(w, req)

		resp := w.Result()
		if resp.StatusCode != http.StatusNotFound {
			t.Errorf("expected status 404, got %d", resp.StatusCode)
		}
	})

	t.Run("after callback captured", func(t *testing.T) {
		// Simulate callback capture
		captureReq := httptest.NewRequest(http.MethodGet, "/cb/"+session.ID+"/callback?code=ABC&state=XYZ", nil)
		captureReq.SetPathValue("session", session.ID)
		captureReq.SetPathValue("path", "callback")
		captureW := httptest.NewRecorder()
		s.handleCallbackCapture(captureW, captureReq)

		// Now get data
		req := httptest.NewRequest(http.MethodGet, "/callback/"+session.ID+"/data", nil)
		req.SetPathValue("session", session.ID)
		w := httptest.NewRecorder()

		s.handleCallbackGetData(w, req)

		resp := w.Result()
		if resp.StatusCode != http.StatusOK {
			t.Errorf("expected status 200, got %d", resp.StatusCode)
		}

		var result callbackDataResponse
		json.NewDecoder(resp.Body).Decode(&result)
		if !result.Received {
			t.Error("expected received=true")
		}
		if result.Callback == nil {
			t.Fatal("expected callback data")
		}
		if result.Callback.Query != "code=ABC&state=XYZ" {
			t.Errorf("expected query 'code=ABC&state=XYZ', got %q", result.Callback.Query)
		}
	})
}

func TestServerCallbackDelete(t *testing.T) {
	s := NewServer(18374, logger.Nop(), "")
	defer s.Stop(context.Background())

	session, _ := s.callbackChannel.Register(8080, "/callback", 5*time.Minute)

	t.Run("delete existing session", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodDelete, "/callback/"+session.ID, nil)
		req.SetPathValue("session", session.ID)
		w := httptest.NewRecorder()

		s.handleCallbackDelete(w, req)

		resp := w.Result()
		if resp.StatusCode != http.StatusOK {
			t.Errorf("expected status 200, got %d", resp.StatusCode)
		}

		var result callbackDeleteResponse
		json.NewDecoder(resp.Body).Decode(&result)
		if !result.Success {
			t.Error("expected success")
		}

		// Verify session is gone
		if s.sessionStore.Get(session.ID) != nil {
			t.Error("expected session to be deleted")
		}
	})

	t.Run("delete nonexistent (still returns success)", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodDelete, "/callback/nonexistent", nil)
		req.SetPathValue("session", "nonexistent")
		w := httptest.NewRecorder()

		s.handleCallbackDelete(w, req)

		resp := w.Result()
		if resp.StatusCode != http.StatusOK {
			t.Errorf("expected status 200, got %d", resp.StatusCode)
		}
	})
}

func TestServerCallbackCapture(t *testing.T) {
	s := NewServer(18374, logger.Nop(), "")
	defer s.Stop(context.Background())

	t.Run("capture valid callback", func(t *testing.T) {
		session, _ := s.callbackChannel.Register(8080, "/callback", 5*time.Minute)

		req := httptest.NewRequest(http.MethodGet, "/cb/"+session.ID+"/callback?code=TOKEN123&state=STATE456", nil)
		req.SetPathValue("session", session.ID)
		req.SetPathValue("path", "callback")
		w := httptest.NewRecorder()

		s.handleCallbackCapture(w, req)

		resp := w.Result()
		if resp.StatusCode != http.StatusOK {
			t.Errorf("expected status 200, got %d", resp.StatusCode)
		}

		// Verify HTML response
		if !strings.Contains(resp.Header.Get("Content-Type"), "text/html") {
			t.Error("expected HTML content type")
		}

		// Verify callback was captured
		data, received := s.callbackChannel.GetData(session.ID)
		if !received {
			t.Error("expected callback to be captured")
		}
		if data.Query != "code=TOKEN123&state=STATE456" {
			t.Errorf("expected query to be captured, got %q", data.Query)
		}
	})

	t.Run("nonexistent session returns 404", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/cb/nonexistent/callback", nil)
		req.SetPathValue("session", "nonexistent")
		req.SetPathValue("path", "callback")
		w := httptest.NewRecorder()

		s.handleCallbackCapture(w, req)

		resp := w.Result()
		if resp.StatusCode != http.StatusNotFound {
			t.Errorf("expected status 404, got %d", resp.StatusCode)
		}
	})

	t.Run("duplicate callback returns success (single-use)", func(t *testing.T) {
		session, _ := s.callbackChannel.Register(8080, "/callback", 5*time.Minute)

		// First callback
		req1 := httptest.NewRequest(http.MethodGet, "/cb/"+session.ID+"/callback?code=FIRST", nil)
		req1.SetPathValue("session", session.ID)
		req1.SetPathValue("path", "callback")
		s.handleCallbackCapture(httptest.NewRecorder(), req1)

		// Second callback - should still return 200 for browser UX
		req2 := httptest.NewRequest(http.MethodGet, "/cb/"+session.ID+"/callback?code=SECOND", nil)
		req2.SetPathValue("session", session.ID)
		req2.SetPathValue("path", "callback")
		w := httptest.NewRecorder()

		s.handleCallbackCapture(w, req2)

		resp := w.Result()
		// Still returns 200 for good browser UX
		if resp.StatusCode != http.StatusOK {
			t.Errorf("expected status 200 for duplicate callback, got %d", resp.StatusCode)
		}

		// But original data is preserved
		data, _ := s.callbackChannel.GetData(session.ID)
		if data.Query != "code=FIRST" {
			t.Errorf("expected first callback query preserved, got %q", data.Query)
		}
	})
}

func TestServerStopCleansUpSessions(t *testing.T) {
	s := NewServer(18374, logger.Nop(), "")

	// Create some sessions
	s.callbackChannel.Register(8080, "/callback", 5*time.Minute)
	s.callbackChannel.Register(8081, "/callback", 5*time.Minute)

	if s.sessionStore.Count() != 2 {
		t.Errorf("expected 2 sessions, got %d", s.sessionStore.Count())
	}

	// Stop should clean up
	s.Stop(context.Background())

	// Session store should be stopped (can't easily verify internal state,
	// but we can verify stop is idempotent)
	s.Stop(context.Background()) // Should not panic
}

func TestServer_DynamicListenerStartStop(t *testing.T) {
	s := NewServer(18374, logger.Nop(), "")
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		s.Stop(ctx)
	})

	// Get a free port for the dynamic listener
	port := getFreePort(t)

	// Register a callback session which starts a dynamic listener
	req := httptest.NewRequest(http.MethodPost, "/callback/register", bytes.NewBufferString(fmt.Sprintf(`{"port": %d}`, port)))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.handleCallbackRegister(w, req)

	resp := w.Result()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected status 200, got %d", resp.StatusCode)
	}

	var result callbackRegisterResponse
	json.NewDecoder(resp.Body).Decode(&result)
	if !result.Success || result.SessionID == "" {
		t.Fatalf("expected successful registration, got error: %s", result.Error)
	}

	// Small delay for listener to start
	time.Sleep(50 * time.Millisecond)

	// Verify the listener is actually listening on the port
	conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 1*time.Second)
	if err != nil {
		t.Fatalf("expected dynamic listener to be listening on port %d: %v", port, err)
	}
	conn.Close()

	if ln, err := net.Listen("tcp", "[::1]:0"); err == nil {
		ln.Close()
		conn6, err := net.DialTimeout("tcp", fmt.Sprintf("[::1]:%d", port), 1*time.Second)
		if err != nil {
			t.Fatalf("expected dynamic listener to be listening on IPv6 port %d: %v", port, err)
		}
		conn6.Close()
	}

	// Delete the session which should stop the listener
	delReq := httptest.NewRequest(http.MethodDelete, "/callback/"+result.SessionID, nil)
	delReq.SetPathValue("session", result.SessionID)
	delW := httptest.NewRecorder()
	s.handleCallbackDelete(delW, delReq)

	// Small delay for listener to stop
	time.Sleep(50 * time.Millisecond)

	// Verify the listener has stopped
	conn, err = net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond)
	if err == nil {
		conn.Close()
		t.Errorf("expected dynamic listener to be stopped, but port %d is still open", port)
	}

	if ln, err := net.Listen("tcp", "[::1]:0"); err == nil {
		ln.Close()
		conn6, err := net.DialTimeout("tcp", fmt.Sprintf("[::1]:%d", port), 100*time.Millisecond)
		if err == nil {
			conn6.Close()
			t.Errorf("expected IPv6 dynamic listener to be stopped, but port %d is still open", port)
		}
	}
}

func TestServer_DynamicListenerCapture(t *testing.T) {
	s := NewServer(18374, logger.Nop(), "")
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		s.Stop(ctx)
	})

	// Get a free port for the dynamic listener
	port := getFreePort(t)

	// Register a callback session
	req := httptest.NewRequest(http.MethodPost, "/callback/register", bytes.NewBufferString(fmt.Sprintf(`{"port": %d}`, port)))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.handleCallbackRegister(w, req)

	var result callbackRegisterResponse
	json.NewDecoder(w.Result().Body).Decode(&result)
	sessionID := result.SessionID

	// Small delay for listener to start
	time.Sleep(50 * time.Millisecond)

	// Make a request to the dynamic listener (simulating OAuth callback)
	callbackURL := fmt.Sprintf("http://127.0.0.1:%d/callback?code=AUTH_CODE&state=RANDOM_STATE", port)
	resp, err := http.Get(callbackURL)
	if err != nil {
		t.Fatalf("failed to make callback request: %v", err)
	}
	resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200 from callback, got %d", resp.StatusCode)
	}

	// Verify the callback was captured
	data, received := s.callbackChannel.GetData(sessionID)
	if !received {
		t.Error("expected callback to be captured")
	}
	if data == nil {
		t.Fatal("expected callback data")
	}
	if !strings.Contains(data.Query, "code=AUTH_CODE") {
		t.Errorf("expected query to contain 'code=AUTH_CODE', got %q", data.Query)
	}
	if !strings.Contains(data.Query, "state=RANDOM_STATE") {
		t.Errorf("expected query to contain 'state=RANDOM_STATE', got %q", data.Query)
	}
}

func TestServer_DynamicListenerCaptureIPv6(t *testing.T) {
	s := NewServer(18374, logger.Nop(), "")
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		s.Stop(ctx)
	})

	if ln, err := net.Listen("tcp", "[::1]:0"); err != nil {
		t.Skip("IPv6 loopback not available")
	} else {
		ln.Close()
	}

	port := getFreePort(t)

	req := httptest.NewRequest(http.MethodPost, "/callback/register", bytes.NewBufferString(fmt.Sprintf(`{"port": %d}`, port)))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.handleCallbackRegister(w, req)

	var result callbackRegisterResponse
	json.NewDecoder(w.Result().Body).Decode(&result)
	if !result.Success || result.SessionID == "" {
		t.Fatalf("expected successful registration, got error: %s", result.Error)
	}

	time.Sleep(50 * time.Millisecond)

	callbackURL := fmt.Sprintf("http://[::1]:%d/callback?code=AUTH_CODE_V6&state=RANDOM_STATE_V6", port)
	resp, err := http.Get(callbackURL)
	if err != nil {
		t.Fatalf("failed to make IPv6 callback request: %v", err)
	}
	resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200 from IPv6 callback, got %d", resp.StatusCode)
	}

	data, received := s.callbackChannel.GetData(result.SessionID)
	if !received {
		t.Error("expected IPv6 callback to be captured")
	}
	if data == nil {
		t.Fatal("expected IPv6 callback data")
	}
	if !strings.Contains(data.Query, "code=AUTH_CODE_V6") {
		t.Errorf("expected query to contain 'code=AUTH_CODE_V6', got %q", data.Query)
	}
	if !strings.Contains(data.Query, "state=RANDOM_STATE_V6") {
		t.Errorf("expected query to contain 'state=RANDOM_STATE_V6', got %q", data.Query)
	}
}
