//go:build testing

package agent

import (
	"crypto/ed25519"
	"errors"
	"fmt"
	"net"
	"net/url"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/crypto/ssh"
)

func createTestAgent(t *testing.T) *Agent {
	dataDir := t.TempDir()
	agent, err := NewAgent(dataDir)
	require.NoError(t, err)
	return agent
}

func createTestServerOptions(t *testing.T) ServerOptions {
	// Generate test key pair
	_, privKey, err := ed25519.GenerateKey(nil)
	require.NoError(t, err)
	sshPubKey, err := ssh.NewPublicKey(privKey.Public().(ed25519.PublicKey))
	require.NoError(t, err)

	// Find available port
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	port := listener.Addr().(*net.TCPAddr).Port
	listener.Close()

	return ServerOptions{
		Network: "tcp",
		Addr:    fmt.Sprintf("127.0.0.1:%d", port),
		Keys:    []ssh.PublicKey{sshPubKey},
	}
}

// TestConnectionManager_NewConnectionManager tests connection manager creation
func TestConnectionManager_NewConnectionManager(t *testing.T) {
	agent := createTestAgent(t)
	cm := newConnectionManager(agent)

	assert.NotNil(t, cm, "Connection manager should not be nil")
	assert.Equal(t, agent, cm.agent, "Agent reference should be set")
	assert.Equal(t, Disconnected, cm.State, "Initial state should be Disconnected")
	assert.Nil(t, cm.eventChan, "Event channel should be nil initially")
	assert.Nil(t, cm.wsClient, "WebSocket client should be nil initially")
	assert.Nil(t, cm.wsTicker, "WebSocket ticker should be nil initially")
	assert.False(t, cm.isConnecting, "isConnecting should be false initially")
}

// TestConnectionManager_StateTransitions tests basic state transitions
func TestConnectionManager_StateTransitions(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager
	initialState := cm.State
	cm.wsClient = &WebSocketClient{
		hubURL: &url.URL{
			Host: "localhost:8080",
		},
	}
	assert.NotNil(t, cm, "Connection manager should not be nil")
	assert.Equal(t, Disconnected, initialState, "Initial state should be Disconnected")

	// Test state transitions
	cm.handleStateChange(WebSocketConnected)
	assert.Equal(t, WebSocketConnected, cm.State, "State should change to WebSocketConnected")

	cm.handleStateChange(SSHConnected)
	assert.Equal(t, SSHConnected, cm.State, "State should change to SSHConnected")

	cm.handleStateChange(Disconnected)
	assert.Equal(t, Disconnected, cm.State, "State should change to Disconnected")

	// Test that same state doesn't trigger changes
	cm.State = WebSocketConnected
	cm.handleStateChange(WebSocketConnected)
	assert.Equal(t, WebSocketConnected, cm.State, "Same state should not trigger change")
}

// TestConnectionManager_EventHandling tests event handling logic
func TestConnectionManager_EventHandling(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager
	cm.wsClient = &WebSocketClient{
		hubURL: &url.URL{
			Host: "localhost:8080",
		},
	}

	testCases := []struct {
		name          string
		initialState  ConnectionState
		event         ConnectionEvent
		expectedState ConnectionState
	}{
		{
			name:          "WebSocket connect from disconnected",
			initialState:  Disconnected,
			event:         WebSocketConnect,
			expectedState: WebSocketConnected,
		},
		{
			name:          "SSH connect from disconnected",
			initialState:  Disconnected,
			event:         SSHConnect,
			expectedState: SSHConnected,
		},
		{
			name:          "WebSocket disconnect from connected",
			initialState:  WebSocketConnected,
			event:         WebSocketDisconnect,
			expectedState: Disconnected,
		},
		{
			name:          "SSH disconnect from connected",
			initialState:  SSHConnected,
			event:         SSHDisconnect,
			expectedState: Disconnected,
		},
		{
			name:          "WebSocket disconnect from SSH connected (no change)",
			initialState:  SSHConnected,
			event:         WebSocketDisconnect,
			expectedState: SSHConnected,
		},
		{
			name:          "SSH disconnect from WebSocket connected (no change)",
			initialState:  WebSocketConnected,
			event:         SSHDisconnect,
			expectedState: WebSocketConnected,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			cm.State = tc.initialState
			cm.handleEvent(tc.event)
			assert.Equal(t, tc.expectedState, cm.State, "State should match expected after event")
		})
	}
}

// TestConnectionManager_TickerManagement tests WebSocket ticker management
func TestConnectionManager_TickerManagement(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager

	// Test starting ticker
	cm.startWsTicker()
	assert.NotNil(t, cm.wsTicker, "Ticker should be created")

	// Test stopping ticker (should not panic)
	assert.NotPanics(t, func() {
		cm.stopWsTicker()
	}, "Stopping ticker should not panic")

	// Test stopping nil ticker (should not panic)
	cm.wsTicker = nil
	assert.NotPanics(t, func() {
		cm.stopWsTicker()
	}, "Stopping nil ticker should not panic")

	// Test restarting ticker
	cm.startWsTicker()
	assert.NotNil(t, cm.wsTicker, "Ticker should be recreated")

	// Test resetting existing ticker
	firstTicker := cm.wsTicker
	cm.startWsTicker()
	assert.Equal(t, firstTicker, cm.wsTicker, "Same ticker instance should be reused")

	cm.stopWsTicker()
}

// TestConnectionManager_WebSocketConnectionFlow tests WebSocket connection logic
func TestConnectionManager_WebSocketConnectionFlow(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager

	// Test WebSocket connection without proper environment
	err := cm.startWebSocketConnection()
	assert.Error(t, err, "WebSocket connection should fail without proper environment")
	assert.Equal(t, Disconnected, cm.State, "State should remain Disconnected after failed connection")

	// Test with invalid URL
	t.Setenv("BESZEL_AGENT_HUB_URL", "1,33%")
	t.Setenv("BESZEL_AGENT_TOKEN", "test-token")

	_, err2 := newWebSocketClient(agent)
	assert.Error(t, err2, "WebSocket client creation should fail with invalid URL")

	// Test with missing token
	t.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
	t.Setenv("BESZEL_AGENT_TOKEN", "")

	_, err3 := newWebSocketClient(agent)
	assert.Error(t, err3, "WebSocket client creation should fail without token")
}

// TestConnectionManager_ReconnectionLogic tests reconnection prevention logic
func TestConnectionManager_ReconnectionLogic(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager
	cm.eventChan = make(chan ConnectionEvent, 1)

	// Test that isConnecting flag prevents duplicate reconnection attempts
	// Start from connected state, then simulate disconnect
	cm.State = WebSocketConnected
	cm.isConnecting = false

	// First disconnect should trigger reconnection logic
	cm.handleStateChange(Disconnected)
	assert.Equal(t, Disconnected, cm.State, "Should change to disconnected")
	assert.True(t, cm.isConnecting, "Should set isConnecting flag")
}

// TestConnectionManager_ConnectWithRateLimit tests connection rate limiting
func TestConnectionManager_ConnectWithRateLimit(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager

	// Set up environment for WebSocket client creation
	t.Setenv("BESZEL_AGENT_HUB_URL", "ws://localhost:8080")
	t.Setenv("BESZEL_AGENT_TOKEN", "test-token")

	// Create WebSocket client
	wsClient, err := newWebSocketClient(agent)
	require.NoError(t, err)
	cm.wsClient = wsClient

	// Set recent connection attempt
	cm.wsClient.lastConnectAttempt = time.Now()

	// Test that connection is rate limited
	err = cm.startWebSocketConnection()
	assert.Error(t, err, "Should error due to rate limiting")
	assert.Contains(t, err.Error(), "already connecting", "Error should indicate rate limiting")

	// Test connection after rate limit expires
	cm.wsClient.lastConnectAttempt = time.Now().Add(-10 * time.Second)
	err = cm.startWebSocketConnection()
	// This will fail due to no actual server, but should not be rate limited
	assert.Error(t, err, "Connection should fail but not due to rate limiting")
	assert.NotContains(t, err.Error(), "already connecting", "Error should not indicate rate limiting")
}

// TestConnectionManager_StartWithInvalidConfig tests starting with invalid configuration
func TestConnectionManager_StartWithInvalidConfig(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager
	serverOptions := createTestServerOptions(t)

	// Test starting when already started
	cm.eventChan = make(chan ConnectionEvent, 5)
	err := cm.Start(serverOptions)
	assert.Error(t, err, "Should error when starting already started connection manager")
}

// TestConnectionManager_CloseWebSocket tests WebSocket closing
func TestConnectionManager_CloseWebSocket(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager

	// Test closing when no WebSocket client exists
	assert.NotPanics(t, func() {
		cm.closeWebSocket()
	}, "Should not panic when closing nil WebSocket client")

	// Set up environment and create WebSocket client
	t.Setenv("BESZEL_AGENT_HUB_URL", "ws://localhost:8080")
	t.Setenv("BESZEL_AGENT_TOKEN", "test-token")

	wsClient, err := newWebSocketClient(agent)
	require.NoError(t, err)
	cm.wsClient = wsClient

	// Test closing when WebSocket client exists
	assert.NotPanics(t, func() {
		cm.closeWebSocket()
	}, "Should not panic when closing WebSocket client")
}

// TestConnectionManager_ConnectFlow tests the connect method
func TestConnectionManager_ConnectFlow(t *testing.T) {
	agent := createTestAgent(t)
	cm := agent.connectionManager

	// Test connect without WebSocket client
	assert.NotPanics(t, func() {
		cm.connect()
	}, "Connect should not panic without WebSocket client")
}

func TestShouldExitOnErr(t *testing.T) {
	createDialErr := func(msg string) error {
		return &net.OpError{
			Op:  "dial",
			Net: "tcp",
			Err: errors.New(msg),
		}
	}

	tests := []struct {
		name     string
		err      error
		envValue string
		expected bool
	}{
		{
			name:     "no env var",
			err:      createDialErr("lookup lkahsdfasdf: no such host"),
			envValue: "",
			expected: false,
		},
		{
			name:     "env var false",
			err:      createDialErr("lookup lkahsdfasdf: no such host"),
			envValue: "false",
			expected: false,
		},
		{
			name:     "env var true, matching error",
			err:      createDialErr("lookup lkahsdfasdf: no such host"),
			envValue: "true",
			expected: true,
		},
		{
			name:     "env var true, matching error with extra context",
			err:      createDialErr("lookup beszel.server.lan on [::1]:53: read udp [::1]:44557->[::1]:53: read: connection refused"),
			envValue: "true",
			expected: true,
		},
		{
			name:     "env var true, non-matching error",
			err:      errors.New("connection refused"),
			envValue: "true",
			expected: false,
		},
		{
			name:     "env var true, dial but not lookup",
			err:      createDialErr("connection timeout"),
			envValue: "true",
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("EXIT_ON_DNS_ERROR", tt.envValue)
			result := shouldExitOnErr(tt.err)
			assert.Equal(t, tt.expected, result)
		})
	}
}


"""
Test multi-tab operations: creation, switching, closing, and background tabs.

Tests verify that:
1. Agent can create multiple tabs (3) and switch between them
2. Agent can close tabs with vision=True
3. Agent can handle buttons that open new tabs in background
4. Agent can continue and call done() after each tab operation
5. Browser state doesn't timeout during background tab operations

All tests use:
- max_steps=5 to allow multiple tab operations
- 120s timeout to fail if test takes too long
- Mock LLM to verify agent can still make decisions after tab operations

Usage:
	uv run pytest tests/ci/browser/test_tabs.py -v -s
"""

import asyncio
import time

import pytest
from pytest_httpserver import HTTPServer

from browser_use.agent.service import Agent
from browser_use.browser import BrowserSession
from browser_use.browser.profile import BrowserProfile
from tests.ci.conftest import create_mock_llm

@pytest.fixture(scope='session')
def http_server():
	"""Create and provide a test HTTP server for tab tests."""
	server = HTTPServer()
	server.start()

	# Route 1: Home page
	server.expect_request('/home').respond_with_data(
		'<title>Home Page</title>
Home Page

This is the home page
',
		content_type='text/html',
	)

	# Route 2: Page 1
	server.expect_request('/page1').respond_with_data(
		'<title>Page 1</title>
Page 1

First test page
',
		content_type='text/html',
	)

	# Route 3: Page 2
	server.expect_request('/page2').respond_with_data(
		'<title>Page 2</title>
Page 2

Second test page
',
		content_type='text/html',
	)

	# Route 4: Page 3
	server.expect_request('/page3').respond_with_data(
		'<title>Page 3</title>
Page 3

Third test page
',
		content_type='text/html',
	)

	# Route 5: Background tab page - has a link that opens a new tab in the background
	server.expect_request('/background-tab-test').respond_with_data(
		"""
		<!DOCTYPE html>

		<title>Background Tab Test</title>

Background Tab Test

Click the link below to open a new tab in the background:

			Open New Tab (link)

			<button id="open-tab-btn" onclick="window.open('/page3', '_blank'); document.getElementById('status').textContent='Tab opened!'">
				Open New Tab (button)
			</button>

		""",
		content_type='text/html',
	)

	yield server
	server.stop()

@pytest.fixture(scope='session')
def base_url(http_server):
	"""Return the base URL for the test HTTP server."""
	return f'http://{http_server.host}:{http_server.port}'

@pytest.fixture(scope='function')
async def browser_session():
	"""Create a browser session for tab tests."""
	session = BrowserSession(
		browser_profile=BrowserProfile(
			headless=True,
			user_data_dir=None,
			keep_alive=True,
		)
	)
	await session.start()
	yield session
	await session.kill()

class TestMultiTabOperations:
	"""Test multi-tab creation, switching, and closing."""

	async def test_create_and_switch_three_tabs(self, browser_session, base_url):
		"""Test that agent can create 3 tabs, switch between them, and call done().

		This test verifies that browser state is retrieved between each step.
		"""
		start_time = time.time()

		actions = [
			# Action 1: Navigate to home page
			f"""
			{{
				"thinking": "I'll start by navigating to the home page",
				"evaluation_previous_goal": "Starting task",
				"memory": "Navigating to home page",
				"next_goal": "Navigate to home page",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/home",
							"new_tab": false
						}}
					}}
				]
			}}
			""",
			# Action 2: Open page1 in new tab
			f"""
			{{
				"thinking": "Now I'll open page 1 in a new tab",
				"evaluation_previous_goal": "Home page loaded",
				"memory": "Opening page 1 in new tab",
				"next_goal": "Open page 1 in new tab",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page1",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 3: Open page2 in new tab
			f"""
			{{
				"thinking": "Now I'll open page 2 in a new tab",
				"evaluation_previous_goal": "Page 1 opened in new tab",
				"memory": "Opening page 2 in new tab",
				"next_goal": "Open page 2 in new tab",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page2",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 4: Switch to first tab
			"""
			{
				"thinking": "Now I'll switch back to the first tab",
				"evaluation_previous_goal": "Page 2 opened in new tab",
				"memory": "Switching to first tab",
				"next_goal": "Switch to first tab",
				"action": [
					{
						"switch": {
							"tab_id": "0000"
						}
					}
				]
			}
			""",
			# Action 5: Done
			"""
			{
				"thinking": "I've successfully created 3 tabs and switched between them",
				"evaluation_previous_goal": "Switched to first tab",
				"memory": "All tabs created and switched",
				"next_goal": "Complete task",
				"action": [
					{
						"done": {
							"text": "Successfully created 3 tabs and switched between them",
							"success": true
						}
					}
				]
			}
			""",
		]

		mock_llm = create_mock_llm(actions=actions)

		agent = Agent(
			task=f'Navigate to {base_url}/home, then open {base_url}/page1 and {base_url}/page2 in new tabs, then switch back to the first tab',
			llm=mock_llm,
			browser_session=browser_session,
		)

		# Run with timeout - should complete within 2 minutes
		try:
			history = await asyncio.wait_for(agent.run(max_steps=5), timeout=120)
			elapsed = time.time() - start_time

			print(f'\n⏱  Test completed in {elapsed:.2f} seconds')
			print(f'📊 Completed {len(history)} steps')

			# Verify each step has browser state
			for i, step in enumerate(history.history):
				assert step.state is not None, f'Step {i} should have browser state'
				assert step.state.url is not None, f'Step {i} should have URL in browser state'
				print(f'  Step {i + 1}: URL={step.state.url}, tabs={len(step.state.tabs) if step.state.tabs else 0}')

			assert len(history) >= 4, 'Agent should have completed at least 4 steps'

			# Verify we have 3 tabs open
			tabs = await browser_session.get_tabs()
			assert len(tabs) >= 3, f'Should have at least 3 tabs open, got {len(tabs)}'

			# Verify agent completed successfully
			final_result = history.final_result()
			assert final_result is not None, 'Agent should return a final result'
			assert 'Successfully' in final_result, 'Agent should report success'

			# Note: Test is fast (< 1s) because mock LLM returns instantly and pages are simple,
			# but browser state IS being retrieved correctly between steps as verified above
		except TimeoutError:
			pytest.fail('Test timed out after 2 minutes - agent hung during tab operations')

	async def test_close_tab_with_vision(self, browser_session, base_url):
		"""Test that agent can close a tab with vision=True and call done()."""

		actions = [
			# Action 1: Navigate to home page
			f"""
			{{
				"thinking": "I'll start by navigating to the home page",
				"evaluation_previous_goal": "Starting task",
				"memory": "Navigating to home page",
				"next_goal": "Navigate to home page",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/home",
							"new_tab": false
						}}
					}}
				]
			}}
			""",
			# Action 2: Open page1 in new tab
			f"""
			{{
				"thinking": "Now I'll open page 1 in a new tab",
				"evaluation_previous_goal": "Home page loaded",
				"memory": "Opening page 1 in new tab",
				"next_goal": "Open page 1 in new tab",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page1",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 3: Close the current tab
			"""
			{
				"thinking": "Now I'll close the current tab (page1)",
				"evaluation_previous_goal": "Page 1 opened in new tab",
				"memory": "Closing current tab",
				"next_goal": "Close current tab",
				"action": [
					{
						"close": {
							"tab_id": "0001"
						}
					}
				]
			}
			""",
			# Action 4: Done
			"""
			{
				"thinking": "I've successfully closed the tab",
				"evaluation_previous_goal": "Tab closed",
				"memory": "Tab closed successfully",
				"next_goal": "Complete task",
				"action": [
					{
						"done": {
							"text": "Successfully closed the tab",
							"success": true
						}
					}
				]
			}
			""",
		]

		mock_llm = create_mock_llm(actions=actions)

		agent = Agent(
			task=f'Navigate to {base_url}/home, then open {base_url}/page1 in a new tab, then close the page1 tab',
			llm=mock_llm,
			browser_session=browser_session,
			use_vision=True,  # Enable vision for this test
		)

		# Run with timeout - should complete within 2 minutes
		try:
			history = await asyncio.wait_for(agent.run(max_steps=5), timeout=120)
			assert len(history) >= 3, 'Agent should have completed at least 3 steps'

			# Verify agent completed successfully
			final_result = history.final_result()
			assert final_result is not None, 'Agent should return a final result'
			assert 'Successfully' in final_result, 'Agent should report success'
		except TimeoutError:
			pytest.fail('Test timed out after 2 minutes - agent hung during tab closing with vision')

	async def test_background_tab_open_no_timeout(self, browser_session, base_url):
		"""Test that browser state doesn't timeout when a new tab opens in the background."""
		start_time = time.time()

		actions = [
			# Action 1: Navigate to home page
			f"""
			{{
				"thinking": "I'll navigate to the home page first",
				"evaluation_previous_goal": "Starting task",
				"memory": "Navigating to home page",
				"next_goal": "Navigate to home page",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/home",
							"new_tab": false
						}}
					}}
				]
			}}
			""",
			# Action 2: Open page1 in new background tab (stay on home page)
			f"""
			{{
				"thinking": "I'll open page1 in a new background tab",
				"evaluation_previous_goal": "Home page loaded",
				"memory": "Opening background tab",
				"next_goal": "Open background tab without switching to it",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page1",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 3: Immediately check browser state after background tab opens
			"""
			{
				"thinking": "After opening background tab, browser state should still be accessible",
				"evaluation_previous_goal": "Background tab opened",
				"memory": "Verifying browser state works",
				"next_goal": "Complete task",
				"action": [
					{
						"done": {
							"text": "Successfully opened background tab, browser state remains accessible",
							"success": true
						}
					}
				]
			}
			""",
		]

		mock_llm = create_mock_llm(actions=actions)

		agent = Agent(
			task=f'Navigate to {base_url}/home and open {base_url}/page1 in a new tab',
			llm=mock_llm,
			browser_session=browser_session,
		)

		# Run with timeout - this tests if browser state times out when new tabs open
		try:
			history = await asyncio.wait_for(agent.run(max_steps=3), timeout=120)
			elapsed = time.time() - start_time

			print(f'\n⏱  Test completed in {elapsed:.2f} seconds')
			print(f'📊 Completed {len(history)} steps')

			# Verify each step has browser state (the key test - no timeouts)
			for i, step in enumerate(history.history):
				assert step.state is not None, f'Step {i} should have browser state'
				assert step.state.url is not None, f'Step {i} should have URL in browser state'
				print(f'  Step {i + 1}: URL={step.state.url}, tabs={len(step.state.tabs) if step.state.tabs else 0}')

			assert len(history) >= 2, 'Agent should have completed at least 2 steps'

			# Verify agent completed successfully
			final_result = history.final_result()
			assert final_result is not None, 'Agent should return a final result'
			assert 'Successfully' in final_result, 'Agent should report success'

			# Verify we have at least 2 tabs
			tabs = await browser_session.get_tabs()
			print(f'  Final tab count: {len(tabs)}')
			assert len(tabs) >= 2, f'Should have at least 2 tabs after opening background tab, got {len(tabs)}'

		except TimeoutError:
			pytest.fail('Test timed out after 2 minutes - browser state timed out after opening background tab')

	async def test_rapid_tab_operations_no_timeout(self, browser_session, base_url):
		"""Test that browser state doesn't timeout during rapid tab operations."""

		actions = [
			# Action 1: Navigate to home page
			f"""
			{{
				"thinking": "I'll navigate to the home page",
				"evaluation_previous_goal": "Starting task",
				"memory": "Navigating to home page",
				"next_goal": "Navigate to home page",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/home",
							"new_tab": false
						}}
					}}
				]
			}}
			""",
			# Action 2: Open page1 in new tab
			f"""
			{{
				"thinking": "Opening page1 in new tab",
				"evaluation_previous_goal": "Home page loaded",
				"memory": "Opening page1",
				"next_goal": "Open page1",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page1",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 3: Open page2 in new tab
			f"""
			{{
				"thinking": "Opening page2 in new tab",
				"evaluation_previous_goal": "Page1 opened",
				"memory": "Opening page2",
				"next_goal": "Open page2",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page2",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 4: Open page3 in new tab
			f"""
			{{
				"thinking": "Opening page3 in new tab",
				"evaluation_previous_goal": "Page2 opened",
				"memory": "Opening page3",
				"next_goal": "Open page3",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page3",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 5: Verify browser state is still accessible
			"""
			{
				"thinking": "All tabs opened rapidly, browser state should still be accessible",
				"evaluation_previous_goal": "Page3 opened",
				"memory": "All tabs opened",
				"next_goal": "Complete task",
				"action": [
					{
						"done": {
							"text": "Successfully opened 4 tabs rapidly without timeout",
							"success": true
						}
					}
				]
			}
			""",
		]

		mock_llm = create_mock_llm(actions=actions)

		agent = Agent(
			task='Open multiple tabs rapidly and verify browser state remains accessible',
			llm=mock_llm,
			browser_session=browser_session,
		)

		# Run with timeout - should complete within 2 minutes
		try:
			history = await asyncio.wait_for(agent.run(max_steps=5), timeout=120)
			assert len(history) >= 4, 'Agent should have completed at least 4 steps'

			# Verify we have 4 tabs open
			tabs = await browser_session.get_tabs()
			assert len(tabs) >= 4, f'Should have at least 4 tabs open, got {len(tabs)}'

			# Verify agent completed successfully
			final_result = history.final_result()
			assert final_result is not None, 'Agent should return a final result'
			assert 'Successfully' in final_result, 'Agent should report success'
		except TimeoutError:
			pytest.fail('Test timed out after 2 minutes - browser state timed out during rapid tab operations')

	async def test_multiple_tab_switches_and_close(self, browser_session, base_url):
		"""Test that agent can switch between multiple tabs and close one."""

		actions = [
			# Action 1: Navigate to home page
			f"""
			{{
				"thinking": "I'll start by navigating to the home page",
				"evaluation_previous_goal": "Starting task",
				"memory": "Navigating to home page",
				"next_goal": "Navigate to home page",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/home",
							"new_tab": false
						}}
					}}
				]
			}}
			""",
			# Action 2: Open page1 in new tab
			f"""
			{{
				"thinking": "Opening page 1 in new tab",
				"evaluation_previous_goal": "Home page loaded",
				"memory": "Opening page 1",
				"next_goal": "Open page 1",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page1",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 3: Open page2 in new tab
			f"""
			{{
				"thinking": "Opening page 2 in new tab",
				"evaluation_previous_goal": "Page 1 opened",
				"memory": "Opening page 2",
				"next_goal": "Open page 2",
				"action": [
					{{
						"navigate": {{
							"url": "{base_url}/page2",
							"new_tab": true
						}}
					}}
				]
			}}
			""",
			# Action 4: Switch to tab 1
			"""
			{
				"thinking": "Switching to tab 1 (page1)",
				"evaluation_previous_goal": "Page 2 opened",
				"memory": "Switching to page 1",
				"next_goal": "Switch to page 1",
				"action": [
					{
						"switch": {
							"tab_id": "0001"
						}
					}
				]
			}
			""",
			# Action 5: Close current tab
			"""
			{
				"thinking": "Closing the current tab (page1)",
				"evaluation_previous_goal": "Switched to page 1",
				"memory": "Closing page 1",
				"next_goal": "Close page 1",
				"action": [
					{
						"close": {
							"tab_id": "0001"
						}
					}
				]
			}
			""",
			# Action 6: Done
			"""
			{
				"thinking": "Successfully completed all tab operations",
				"evaluation_previous_goal": "Tab closed",
				"memory": "All operations completed",
				"next_goal": "Complete task",
				"action": [
					{
						"done": {
							"text": "Successfully created, switched, and closed tabs",
							"success": true
						}
					}
				]
			}
			""",
		]

		mock_llm = create_mock_llm(actions=actions)

		agent = Agent(
			task='Create 3 tabs, switch to the second one, then close it',
			llm=mock_llm,
			browser_session=browser_session,
		)

		# Run with timeout - should complete within 2 minutes
		try:
			history = await asyncio.wait_for(agent.run(max_steps=6), timeout=120)
			assert len(history) >= 5, 'Agent should have completed at least 5 steps'

			# Verify agent completed successfully
			final_result = history.final_result()
			assert final_result is not None, 'Agent should return a final result'
			assert 'Successfully' in final_result, 'Agent should report success'
		except TimeoutError:
			pytest.fail('Test timed out after 2 minutes - agent hung during multiple tab operations')


from scrapling.core._types import Any, Awaitable, Unpack
from scrapling.engines._browsers._types import DataRequestParams, GetRequestParams
from scrapling.engines.static import (
    FetcherSession,
    FetcherClient as _FetcherClient,
    AsyncFetcherClient as _AsyncFetcherClient,
)
from scrapling.engines.toolbelt.custom import BaseFetcher, Response

__all__ = ["Fetcher", "AsyncFetcher", "FetcherSession"]

__FetcherClientInstance__ = _FetcherClient()
__AsyncFetcherClientInstance__ = _AsyncFetcherClient()

def _merge_selector_config(cls: type[BaseFetcher], kwargs: Any) -> Any:
    """Merge class-level parser arguments into per-request ``selector_config``.

    Values from ``Fetcher.configure(...)`` act as the base; any explicit
    ``selector_config`` passed on the call overrides them.
    """
    selector_config = kwargs.get("selector_config") or {}
    kwargs["selector_config"] = {**cls._generate_parser_arguments(), **selector_config}
    return kwargs

class Fetcher(BaseFetcher):
    """A basic `Fetcher` class type that can only do basic GET, POST, PUT, and DELETE HTTP requests based on `curl_cffi`."""

    @classmethod
    def get(cls, url: str, **kwargs: Unpack[GetRequestParams]) -> Response:
        return __FetcherClientInstance__.get(url, **_merge_selector_config(cls, kwargs))

    @classmethod
    def post(cls, url: str, **kwargs: Unpack[DataRequestParams]) -> Response:
        return __FetcherClientInstance__.post(url, **_merge_selector_config(cls, kwargs))

    @classmethod
    def put(cls, url: str, **kwargs: Unpack[DataRequestParams]) -> Response:
        return __FetcherClientInstance__.put(url, **_merge_selector_config(cls, kwargs))

    @classmethod
    def delete(cls, url: str, **kwargs: Unpack[DataRequestParams]) -> Response:
        return __FetcherClientInstance__.delete(url, **_merge_selector_config(cls, kwargs))

class AsyncFetcher(BaseFetcher):
    """A basic `Fetcher` class type that can only do basic GET, POST, PUT, and DELETE HTTP requests based on `curl_cffi`."""

    @classmethod
    def get(cls, url: str, **kwargs: Unpack[GetRequestParams]) -> Awaitable[Response]:
        return __AsyncFetcherClientInstance__.get(url, **_merge_selector_config(cls, kwargs))

    @classmethod
    def post(cls, url: str, **kwargs: Unpack[DataRequestParams]) -> Awaitable[Response]:
        return __AsyncFetcherClientInstance__.post(url, **_merge_selector_config(cls, kwargs))

    @classmethod
    def put(cls, url: str, **kwargs: Unpack[DataRequestParams]) -> Awaitable[Response]:
        return __AsyncFetcherClientInstance__.put(url, **_merge_selector_config(cls, kwargs))

    @classmethod
    def delete(cls, url: str, **kwargs: Unpack[DataRequestParams]) -> Awaitable[Response]:
        return __AsyncFetcherClientInstance__.delete(url, **_merge_selector_config(cls, kwargs))


"use client"

import * as React from "react"
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"

import { cn } from "#/lib/utils.ts"
import { HugeiconsIcon } from "@hugeicons/react"
import { ArrowRight01Icon, Tick02Icon } from "@hugeicons/core-free-icons"

function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
  return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}

function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
  return (
    <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
  )
}

function ContextMenuTrigger({
  className,
  ...props
}: ContextMenuPrimitive.Trigger.Props) {
  return (
    <ContextMenuPrimitive.Trigger
      data-slot="context-menu-trigger"
      className={cn("select-none", className)}
      {...props}
    />
  )
}

function ContextMenuContent({
  className,
  align = "start",
  alignOffset = 4,
  side = "right",
  sideOffset = 0,
  ...props
}: ContextMenuPrimitive.Popup.Props &
  Pick<
    ContextMenuPrimitive.Positioner.Props,
    "align" | "alignOffset" | "side" | "sideOffset"
  >) {
  return (
    <ContextMenuPrimitive.Portal>
      <ContextMenuPrimitive.Positioner
        className="isolate z-50 outline-none"
        align={align}
        alignOffset={alignOffset}
        side={side}
        sideOffset={sideOffset}
      >
        <ContextMenuPrimitive.Popup
          data-slot="context-menu-content"
          className={cn("z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
          {...props}
        />
      </ContextMenuPrimitive.Positioner>
    </ContextMenuPrimitive.Portal>
  )
}

function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
  return (
    <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
  )
}

function ContextMenuLabel({
  className,
  inset,
  ...props
}: ContextMenuPrimitive.GroupLabel.Props & {
  inset?: boolean
}) {
  return (
    <ContextMenuPrimitive.GroupLabel
      data-slot="context-menu-label"
      data-inset={inset}
      className={cn(
        "px-2 py-1.5 text-xs text-muted-foreground data-inset:pl-7.5",
        className
      )}
      {...props}
    />
  )
}

function ContextMenuItem({
  className,
  inset,
  variant = "default",
  ...props
}: ContextMenuPrimitive.Item.Props & {
  inset?: boolean
  variant?: "default" | "destructive"
}) {
  return (
    <ContextMenuPrimitive.Item
      data-slot="context-menu-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "group/context-menu-item relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 data-[variant=destructive]:*:[svg]:text-destructive",
        className
      )}
      {...props}
    />
  )
}

function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
  return (
    <ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
  )
}

function ContextMenuSubTrigger({
  className,
  inset,
  children,
  ...props
}: ContextMenuPrimitive.SubmenuTrigger.Props & {
  inset?: boolean
}) {
  return (
    <ContextMenuPrimitive.SubmenuTrigger
      data-slot="context-menu-sub-trigger"
      data-inset={inset}
      className={cn(
        "flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
        className
      )}
      {...props}
    >
      {children}
      <HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} className="ml-auto" />
    </ContextMenuPrimitive.SubmenuTrigger>
  )
}

function ContextMenuSubContent({
  ...props
}: React.ComponentProps<typeof ContextMenuContent>) {
  return (
    <ContextMenuContent
      data-slot="context-menu-sub-content"
      className="shadow-lg"
      side="right"
      {...props}
    />
  )
}

function ContextMenuCheckboxItem({
  className,
  children,
  checked,
  inset,
  ...props
}: ContextMenuPrimitive.CheckboxItem.Props & {
  inset?: boolean
}) {
  return (
    <ContextMenuPrimitive.CheckboxItem
      data-slot="context-menu-checkbox-item"
      data-inset={inset}
      className={cn(
        "relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
        className
      )}
      checked={checked}
      {...props}
    >

        <ContextMenuPrimitive.CheckboxItemIndicator>
          <HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
        </ContextMenuPrimitive.CheckboxItemIndicator>

      {children}
    </ContextMenuPrimitive.CheckboxItem>
  )
}

function ContextMenuRadioGroup({
  ...props
}: ContextMenuPrimitive.RadioGroup.Props) {
  return (
    <ContextMenuPrimitive.RadioGroup
      data-slot="context-menu-radio-group"
      {...props}
    />
  )
}

function ContextMenuRadioItem({
  className,
  children,
  inset,
  ...props
}: ContextMenuPrimitive.RadioItem.Props & {
  inset?: boolean
}) {
  return (
    <ContextMenuPrimitive.RadioItem
      data-slot="context-menu-radio-item"
      data-inset={inset}
      className={cn(
        "relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
        className
      )}
      {...props}
    >

        <ContextMenuPrimitive.RadioItemIndicator>
          <HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
        </ContextMenuPrimitive.RadioItemIndicator>

      {children}
    </ContextMenuPrimitive.RadioItem>
  )
}

function ContextMenuSeparator({
  className,
  ...props
}: ContextMenuPrimitive.Separator.Props) {
  return (
    <ContextMenuPrimitive.Separator
      data-slot="context-menu-separator"
      className={cn("-mx-1 my-1 h-px bg-border/50", className)}
      {...props}
    />
  )
}

function ContextMenuShortcut({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (

  )
}

export {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuGroup,
  ContextMenuPortal,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuRadioGroup,
}


from browser_use.browser import BrowserProfile, BrowserSession

class TestUrlAllowlistSecurity:
	"""Tests for URL allowlist security bypass prevention and URL allowlist glob pattern matching."""

	def test_authentication_bypass_prevention(self):
		"""Test that the URL allowlist cannot be bypassed using authentication credentials."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Create a context config with a sample allowed domain
		browser_profile = BrowserProfile(allowed_domains=['example.com'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Security vulnerability test cases
		# These should all be detected as malicious despite containing "example.com"
		assert watchdog._is_url_allowed('https://example.com:password@malicious.com') is False
		assert watchdog._is_url_allowed('https://example.com@malicious.com') is False
		assert watchdog._is_url_allowed('https://example.com%20@malicious.com') is False
		assert watchdog._is_url_allowed('https://example.com%3A@malicious.com') is False

		# Make sure legitimate auth credentials still work
		assert watchdog._is_url_allowed('https://user:password@example.com') is True

	def test_glob_pattern_matching(self):
		"""Test that glob patterns in allowed_domains work correctly."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Test *.example.com pattern (should match subdomains and main domain)
		browser_profile = BrowserProfile(allowed_domains=['*.example.com'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Should match subdomains
		assert watchdog._is_url_allowed('https://sub.example.com') is True
		assert watchdog._is_url_allowed('https://deep.sub.example.com') is True

		# Should also match main domain
		assert watchdog._is_url_allowed('https://example.com') is True

		# Should not match other domains
		assert watchdog._is_url_allowed('https://notexample.com') is False
		assert watchdog._is_url_allowed('https://example.org') is False

		# Test more complex glob patterns
		browser_profile = BrowserProfile(
			allowed_domains=[
				'*.google.com',
				'https://wiki.org',
				'https://good.com',
				'https://*.test.com',
				'chrome://version',
				'brave://*',
			],
			headless=True,
			user_data_dir=None,
		)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Should match domains ending with google.com
		assert watchdog._is_url_allowed('https://google.com') is True
		assert watchdog._is_url_allowed('https://www.google.com') is True
		assert (
			watchdog._is_url_allowed('https://evilgood.com') is False
		)  # make sure we dont allow *good.com patterns, only *.good.com

		# Should match domains starting with wiki
		assert watchdog._is_url_allowed('http://wiki.org') is False
		assert watchdog._is_url_allowed('https://wiki.org') is True

		# Should not match internal domains because scheme was not provided
		assert watchdog._is_url_allowed('chrome://google.com') is False
		assert watchdog._is_url_allowed('chrome://abc.google.com') is False

		# Test browser internal URLs
		assert watchdog._is_url_allowed('chrome://settings') is False
		assert watchdog._is_url_allowed('chrome://version') is True
		assert watchdog._is_url_allowed('chrome-extension://version/') is False
		assert watchdog._is_url_allowed('brave://anything/') is True
		assert watchdog._is_url_allowed('about:blank') is True
		assert watchdog._is_url_allowed('chrome://new-tab-page/') is True
		assert watchdog._is_url_allowed('chrome://new-tab-page') is True

		# Test security for glob patterns (authentication credentials bypass attempts)
		# These should all be detected as malicious despite containing allowed domain patterns
		assert watchdog._is_url_allowed('https://allowed.example.com:password@notallowed.com') is False
		assert watchdog._is_url_allowed('https://subdomain.example.com@evil.com') is False
		assert watchdog._is_url_allowed('https://sub.example.com%20@malicious.org') is False
		assert watchdog._is_url_allowed('https://anygoogle.com@evil.org') is False

		# Test pattern matching
		assert watchdog._is_url_allowed('https://www.test.com') is True
		assert watchdog._is_url_allowed('https://www.testx.com') is False

	def test_glob_pattern_edge_cases(self):
		"""Test edge cases for glob pattern matching to ensure proper behavior."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Test with domains containing glob pattern in the middle
		browser_profile = BrowserProfile(allowed_domains=['*.google.com', 'https://wiki.org'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Verify that 'wiki*' pattern doesn't match domains that merely contain 'wiki' in the middle
		assert watchdog._is_url_allowed('https://notawiki.com') is False
		assert watchdog._is_url_allowed('https://havewikipages.org') is False
		assert watchdog._is_url_allowed('https://my-wiki-site.com') is False

		# Verify that '*google.com' doesn't match domains that have 'google' in the middle
		assert watchdog._is_url_allowed('https://mygoogle.company.com') is False

		# Create context with potentially risky glob pattern that demonstrates security concerns
		browser_profile = BrowserProfile(allowed_domains=['*.google.com', '*.google.co.uk'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Should match legitimate Google domains
		assert watchdog._is_url_allowed('https://www.google.com') is True
		assert watchdog._is_url_allowed('https://mail.google.co.uk') is True

		# Shouldn't match potentially malicious domains with a similar structure
		# This demonstrates why the previous pattern was risky and why it's now rejected
		assert watchdog._is_url_allowed('https://www.google.evil.com') is False

	def test_automatic_www_subdomain_addition(self):
		"""Test that root domains automatically allow www subdomain."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Test with simple root domains
		browser_profile = BrowserProfile(allowed_domains=['example.com', 'test.org'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Root domain should allow itself
		assert watchdog._is_url_allowed('https://example.com') is True
		assert watchdog._is_url_allowed('https://test.org') is True

		# Root domain should automatically allow www subdomain
		assert watchdog._is_url_allowed('https://www.example.com') is True
		assert watchdog._is_url_allowed('https://www.test.org') is True

		# Should not allow other subdomains
		assert watchdog._is_url_allowed('https://mail.example.com') is False
		assert watchdog._is_url_allowed('https://sub.test.org') is False

		# Should not allow unrelated domains
		assert watchdog._is_url_allowed('https://notexample.com') is False
		assert watchdog._is_url_allowed('https://www.notexample.com') is False

	def test_www_subdomain_not_added_for_country_tlds(self):
		"""Test www subdomain is NOT automatically added for country-specific TLDs (2+ dots)."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Test with country-specific TLDs - these should NOT get automatic www
		browser_profile = BrowserProfile(
			allowed_domains=['example.co.uk', 'test.com.au', 'site.co.jp'], headless=True, user_data_dir=None
		)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Root domains should work exactly as specified
		assert watchdog._is_url_allowed('https://example.co.uk') is True
		assert watchdog._is_url_allowed('https://test.com.au') is True
		assert watchdog._is_url_allowed('https://site.co.jp') is True

		# www subdomains should NOT work automatically (user must specify explicitly)
		assert watchdog._is_url_allowed('https://www.example.co.uk') is False
		assert watchdog._is_url_allowed('https://www.test.com.au') is False
		assert watchdog._is_url_allowed('https://www.site.co.jp') is False

		# Other subdomains should not work
		assert watchdog._is_url_allowed('https://mail.example.co.uk') is False
		assert watchdog._is_url_allowed('https://api.test.com.au') is False

	def test_www_subdomain_not_added_for_existing_subdomains(self):
		"""Test that www is not automatically added for domains that already have subdomains."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Test with existing subdomains - should NOT get automatic www
		browser_profile = BrowserProfile(allowed_domains=['mail.example.com', 'api.test.org'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Exact subdomain should work
		assert watchdog._is_url_allowed('https://mail.example.com') is True
		assert watchdog._is_url_allowed('https://api.test.org') is True

		# www should NOT be automatically added to subdomains
		assert watchdog._is_url_allowed('https://www.mail.example.com') is False
		assert watchdog._is_url_allowed('https://www.api.test.org') is False

		# Root domains should not work either
		assert watchdog._is_url_allowed('https://example.com') is False
		assert watchdog._is_url_allowed('https://test.org') is False

	def test_www_subdomain_not_added_for_wildcard_patterns(self):
		"""Test that www is not automatically added for wildcard patterns."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Test with wildcard patterns - should NOT get automatic www logic
		browser_profile = BrowserProfile(allowed_domains=['*.example.com'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Wildcard should match everything including root and www
		assert watchdog._is_url_allowed('https://example.com') is True
		assert watchdog._is_url_allowed('https://www.example.com') is True
		assert watchdog._is_url_allowed('https://mail.example.com') is True

	def test_www_subdomain_not_added_for_url_patterns(self):
		"""Test that www is not automatically added for full URL patterns."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Test with full URL patterns - should NOT get automatic www logic
		browser_profile = BrowserProfile(
			allowed_domains=['https://example.com', 'http://test.org'], headless=True, user_data_dir=None
		)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Exact URL should work
		assert watchdog._is_url_allowed('https://example.com/path') is True
		assert watchdog._is_url_allowed('http://test.org/page') is True

		# www should NOT be automatically added for full URL patterns
		assert watchdog._is_url_allowed('https://www.example.com') is False
		assert watchdog._is_url_allowed('http://www.test.org') is False

	def test_is_root_domain_helper(self):
		"""Test the _is_root_domain helper method logic."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(allowed_domains=['example.com'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Simple root domains (1 dot) - should return True
		assert watchdog._is_root_domain('example.com') is True
		assert watchdog._is_root_domain('test.org') is True
		assert watchdog._is_root_domain('site.net') is True

		# Subdomains (more than 1 dot) - should return False
		assert watchdog._is_root_domain('www.example.com') is False
		assert watchdog._is_root_domain('mail.example.com') is False
		assert watchdog._is_root_domain('example.co.uk') is False
		assert watchdog._is_root_domain('test.com.au') is False

		# Wildcards - should return False
		assert watchdog._is_root_domain('*.example.com') is False
		assert watchdog._is_root_domain('*example.com') is False

		# Full URLs - should return False
		assert watchdog._is_root_domain('https://example.com') is False
		assert watchdog._is_root_domain('http://test.org') is False

		# Invalid domains - should return False
		assert watchdog._is_root_domain('example') is False
		assert watchdog._is_root_domain('') is False

class TestUrlProhibitlistSecurity:
	"""Tests for URL prohibitlist (blocked domains) behavior and matching semantics."""

	def test_simple_prohibited_domains(self):
		"""Domain-only patterns block exact host and www, but not other subdomains."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(prohibited_domains=['example.com', 'test.org'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Block exact and www
		assert watchdog._is_url_allowed('https://example.com') is False
		assert watchdog._is_url_allowed('https://www.example.com') is False
		assert watchdog._is_url_allowed('https://test.org') is False
		assert watchdog._is_url_allowed('https://www.test.org') is False

		# Allow other subdomains when only root is prohibited
		assert watchdog._is_url_allowed('https://mail.example.com') is True
		assert watchdog._is_url_allowed('https://api.test.org') is True

		# Allow unrelated domains
		assert watchdog._is_url_allowed('https://notexample.com') is True

	def test_glob_pattern_prohibited(self):
		"""Wildcard patterns block subdomains and main domain for http/https only."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(prohibited_domains=['*.example.com'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Block subdomains and main domain
		assert watchdog._is_url_allowed('https://example.com') is False
		assert watchdog._is_url_allowed('https://www.example.com') is False
		assert watchdog._is_url_allowed('https://mail.example.com') is False

		# Allow other domains
		assert watchdog._is_url_allowed('https://notexample.com') is True

		# Wildcard with domain-only should not apply to non-http(s)
		assert watchdog._is_url_allowed('chrome://abc.example.com') is True

	def test_full_url_prohibited_patterns(self):
		"""Full URL patterns block only matching scheme/host/prefix."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(prohibited_domains=['https://wiki.org', 'brave://*'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Scheme-specific blocking
		assert watchdog._is_url_allowed('http://wiki.org') is True
		assert watchdog._is_url_allowed('https://wiki.org') is False
		assert watchdog._is_url_allowed('https://wiki.org/path') is False

		# Internal URL prefix blocking
		assert watchdog._is_url_allowed('brave://anything/') is False
		assert watchdog._is_url_allowed('chrome://settings') is True

	def test_internal_urls_allowed_even_when_prohibited(self):
		"""Internal new-tab/blank URLs are always allowed regardless of prohibited list."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(prohibited_domains=['*'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		assert watchdog._is_url_allowed('about:blank') is True
		assert watchdog._is_url_allowed('chrome://new-tab-page/') is True
		assert watchdog._is_url_allowed('chrome://new-tab-page') is True
		assert watchdog._is_url_allowed('chrome://newtab/') is True

	def test_prohibited_ignored_when_allowlist_present(self):
		"""When allowlist is set, prohibited list is ignored by design."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(
			allowed_domains=['*.example.com'],
			prohibited_domains=['https://example.com'],
			headless=True,
			user_data_dir=None,
		)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Allowed by allowlist even though exact URL is in prohibited list
		assert watchdog._is_url_allowed('https://example.com') is True
		assert watchdog._is_url_allowed('https://www.example.com') is True

		# Not in allowlist => blocked (prohibited list is not consulted in this mode)
		assert watchdog._is_url_allowed('https://api.example.com') is True  # wildcard allowlist includes this
		# A domain outside the allowlist should be blocked
		assert watchdog._is_url_allowed('https://notexample.com') is False

	def test_auth_credentials_do_not_cause_false_block(self):
		"""Credentials injection with prohibited domain in username should not block unrelated hosts."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(prohibited_domains=['example.com'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Host is malicious.com, should not be blocked just because username contains example.com
		assert watchdog._is_url_allowed('https://example.com:password@malicious.com') is True
		assert watchdog._is_url_allowed('https://example.com@malicious.com') is True
		assert watchdog._is_url_allowed('https://example.com%20@malicious.com') is True
		assert watchdog._is_url_allowed('https://example.com%3A@malicious.com') is True

		# Legitimate credentials to a prohibited host should be blocked
		assert watchdog._is_url_allowed('https://user:password@example.com') is False

	def test_case_insensitive_prohibited_domains(self):
		"""Prohibited domain matching should be case-insensitive."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(prohibited_domains=['Example.COM'], headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		assert watchdog._is_url_allowed('https://example.com') is False
		assert watchdog._is_url_allowed('https://WWW.EXAMPLE.COM') is False
		assert watchdog._is_url_allowed('https://mail.example.com') is True

class TestDomainListOptimization:
	"""Tests for domain list optimization (set conversion for large lists)."""

	def test_small_list_keeps_pattern_support(self):
		"""Test that lists < 100 items keep pattern matching support."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		browser_profile = BrowserProfile(
			prohibited_domains=['*.google.com', 'x.com', 'facebook.com'], headless=True, user_data_dir=None
		)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Should still be a list
		assert isinstance(browser_session.browser_profile.prohibited_domains, list)

		# Pattern matching should work
		assert watchdog._is_url_allowed('https://www.google.com') is False
		assert watchdog._is_url_allowed('https://mail.google.com') is False
		assert watchdog._is_url_allowed('https://google.com') is False

		# Exact matches should work
		assert watchdog._is_url_allowed('https://x.com') is False
		assert watchdog._is_url_allowed('https://facebook.com') is False

		# Other domains should be allowed
		assert watchdog._is_url_allowed('https://example.com') is True

	def test_large_list_converts_to_set(self):
		"""Test that lists >= 100 items are converted to sets."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Create a list of 100 domains
		large_list = [f'blocked{i}.com' for i in range(100)]

		browser_profile = BrowserProfile(prohibited_domains=large_list, headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Should be converted to set
		assert isinstance(browser_session.browser_profile.prohibited_domains, set)
		assert len(browser_session.browser_profile.prohibited_domains) == 100

		# Exact matches should work
		assert watchdog._is_url_allowed('https://blocked0.com') is False
		assert watchdog._is_url_allowed('https://blocked50.com') is False
		assert watchdog._is_url_allowed('https://blocked99.com') is False

		# Other domains should be allowed
		assert watchdog._is_url_allowed('https://example.com') is True
		assert watchdog._is_url_allowed('https://blocked100.com') is True  # Not in list

	def test_www_variant_matching_with_sets(self):
		"""Test that www variants are checked in set-based lookups."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Create a list with 100 domains (some with www, some without)
		large_list = [f'site{i}.com' for i in range(50)] + [f'www.domain{i}.org' for i in range(50)]

		browser_profile = BrowserProfile(prohibited_domains=large_list, headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Should be converted to set
		assert isinstance(browser_session.browser_profile.prohibited_domains, set)

		# Test www variant matching for domains without www prefix
		assert watchdog._is_url_allowed('https://site0.com') is False
		assert watchdog._is_url_allowed('https://www.site0.com') is False  # Should also be blocked

		# Test www variant matching for domains with www prefix
		assert watchdog._is_url_allowed('https://www.domain0.org') is False
		assert watchdog._is_url_allowed('https://domain0.org') is False  # Should also be blocked

		# Test that unrelated domains are allowed
		assert watchdog._is_url_allowed('https://example.com') is True
		assert watchdog._is_url_allowed('https://www.example.com') is True

	def test_allowed_domains_with_sets(self):
		"""Test that allowed_domains also works with set optimization."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		# Create a large allowlist
		large_list = [f'allowed{i}.com' for i in range(100)]

		browser_profile = BrowserProfile(allowed_domains=large_list, headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Should be converted to set
		assert isinstance(browser_session.browser_profile.allowed_domains, set)

		# Allowed domains should work
		assert watchdog._is_url_allowed('https://allowed0.com') is True
		assert watchdog._is_url_allowed('https://www.allowed0.com') is True
		assert watchdog._is_url_allowed('https://allowed99.com') is True

		# Other domains should be blocked
		assert watchdog._is_url_allowed('https://example.com') is False
		assert watchdog._is_url_allowed('https://notallowed.com') is False

	def test_manual_set_input(self):
		"""Test that users can directly provide a set."""
		from bubus import EventBus

		from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog

		blocked_set = {f'blocked{i}.com' for i in range(50)}

		browser_profile = BrowserProfile(prohibited_domains=blocked_set, headless=True, user_data_dir=None)
		browser_session = BrowserSession(browser_profile=browser_profile)
		event_bus = EventBus()
		watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)

		# Should remain a set
		assert isinstance(browser_session.browser_profile.prohibited_domains, set)

		# Should work correctly
		assert watchdog._is_url_allowed('https://blocked0.com') is False
		assert watchdog._is_url_allowed('https://example.com') is True