import asyncio
import base64
import os
import pytest
from browser_use.browser.context import BrowserContext, BrowserContextConfig
from browser_use.browser.views import BrowserState
from browser_use.dom.views import DOMElementNode
from unittest.mock import Mock

def test_is_url_allowed():
    """
    Test the _is_url_allowed method to verify that it correctly checks URLs against 
    the allowed domains configuration.
    Scenario 1: When allowed_domains is None, all URLs should be allowed.
    Scenario 2: When allowed_domains is a list, only URLs matching the allowed domain(s) are allowed.
    Scenario 3: When the URL is malformed, it should return False.
    """
    # Create a dummy Browser mock. Only the 'config' attribute is needed for _is_url_allowed.
    dummy_browser = Mock()
    # Set an empty config for dummy_browser; it won't be used in _is_url_allowed.
    dummy_browser.config = Mock()
    # Scenario 1: allowed_domains is None, any URL should be allowed.
    config1 = BrowserContextConfig(allowed_domains=None)
    context1 = BrowserContext(browser=dummy_browser, config=config1)
    assert context1._is_url_allowed("http://anydomain.com") is True
    assert context1._is_url_allowed("https://anotherdomain.org/path") is True
    # Scenario 2: allowed_domains is provided.
    allowed = ["example.com", "mysite.org"]
    config2 = BrowserContextConfig(allowed_domains=allowed)
    context2 = BrowserContext(browser=dummy_browser, config=config2)
    # URL exactly matching
    assert context2._is_url_allowed("http://example.com") is True
    # URL with subdomain (should be allowed)
    assert context2._is_url_allowed("http://sub.example.com/path") is True
    # URL with different domain (should not be allowed)
    assert context2._is_url_allowed("http://notexample.com") is False
    # URL that matches second allowed domain
    assert context2._is_url_allowed("https://mysite.org/page") is True
    # URL with port number, still allowed (port is stripped)
    assert context2._is_url_allowed("http://example.com:8080") is True
    # Scenario 3: Malformed URL or empty domain
    # urlparse will return an empty netloc for some malformed URLs.
    assert context2._is_url_allowed("notaurl") is False
def test_convert_simple_xpath_to_css_selector():
    """
    Test the _convert_simple_xpath_to_css_selector method of BrowserContext.
    This verifies that simple XPath expressions (with and without indices) are correctly converted to CSS selectors.
    """
    # Test empty xpath returns empty string
    assert BrowserContext._convert_simple_xpath_to_css_selector('') == ''
    # Test a simple xpath without indices
    xpath = "/html/body/div/span"
    expected = "html > body > div > span"
    result = BrowserContext._convert_simple_xpath_to_css_selector(xpath)
    assert result == expected
    # Test xpath with an index on one element: [2] should translate to :nth-of-type(2)
    xpath = "/html/body/div[2]/span"
    expected = "html > body > div:nth-of-type(2) > span"
    result = BrowserContext._convert_simple_xpath_to_css_selector(xpath)
    assert result == expected
    # Test xpath with indices on multiple elements:
    # For "li[3]" -> li:nth-of-type(3) and for "a[1]" -> a:nth-of-type(1)
    xpath = "/ul/li[3]/a[1]"
    expected = "ul > li:nth-of-type(3) > a:nth-of-type(1)"
    result = BrowserContext._convert_simple_xpath_to_css_selector(xpath)
    assert result == expected
def test_get_initial_state():
    """
    Test the _get_initial_state method to verify it returns the correct initial BrowserState.
    The test checks that when a dummy page with a URL is provided,
    the returned state contains that URL and other default values.
    """
    # Create a dummy browser since only its existence is needed.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    # Define a dummy page with a 'url' attribute.
    class DummyPage:
        url = "http://dummy.com"
    dummy_page = DummyPage()
    # Call _get_initial_state with a page: URL should be set from page.url.
    state_with_page = context._get_initial_state(page=dummy_page)
    assert state_with_page.url == dummy_page.url
    # Verify that the element_tree is initialized with tag 'root'
    assert state_with_page.element_tree.tag_name == 'root'
    # Call _get_initial_state without a page: URL should be empty.
    state_without_page = context._get_initial_state()
    assert state_without_page.url == ""
@pytest.mark.asyncio
async def test_execute_javascript():
    """
    Test the execute_javascript method by mocking the current page's evaluate function.
    This ensures that when execute_javascript is called, it correctly returns the value
    from the page's evaluate method.
    """
    # Define a dummy page with an async evaluate method.
    class DummyPage:
        async def evaluate(self, script):
            return "dummy_result"
    # Create a dummy session object with a dummy current_page.
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = DummyPage()
    # Create a dummy browser mock with a minimal config.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize the BrowserContext with the dummy browser and config.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    # Manually set the session to our dummy session.
    context.session = dummy_session
    # Call execute_javascript and verify it returns the expected result.
    result = await context.execute_javascript("return 1+1")
    assert result == "dummy_result"
@pytest.mark.asyncio
async def test_enhanced_css_selector_for_element():
    """
    Test the _enhanced_css_selector_for_element method to verify that
    it returns the correct CSS selector string for a dummy DOMElementNode.
    The test checks that:
      - The provided xpath is correctly converted (handling indices),
      - Class attributes are appended as CSS classes,
      - Standard and dynamic attributes (including ones with special characters)
        are correctly added to the selector.
    """
    # Create a dummy DOMElementNode instance with a complex set of attributes.
    dummy_element = DOMElementNode(
        tag_name="div",
        is_visible=True,
        parent=None,
        xpath="/html/body/div[2]",
        attributes={
            "class": "foo bar",
            "id": "my-id",
            "placeholder": 'some "quoted" text',
            "data-testid": "123"
        },
        children=[]
    )
    # Call the method with include_dynamic_attributes=True.
    actual_selector = BrowserContext._enhanced_css_selector_for_element(dummy_element, include_dynamic_attributes=True)
    # Expected conversion:
    # 1. The xpath "/html/body/div[2]" converts to "html > body > div:nth-of-type(2)".
    # 2. The class attribute "foo bar" appends ".foo.bar".
    # 3. The "id" attribute is added as [id="my-id"].
    # 4. The "placeholder" attribute contains quotes; it is added as
    #    [placeholder*="some \"quoted\" text"].
    # 5. The dynamic attribute "data-testid" is added as [data-testid="123"].
    expected_selector = 'html > body > div:nth-of-type(2).foo.bar[id="my-id"][placeholder*="some \\"quoted\\" text"][data-testid="123"]'
    assert actual_selector == expected_selector, f"Expected {expected_selector}, but got {actual_selector}"
@pytest.mark.asyncio
async def test_get_scroll_info():
    """
    Test the get_scroll_info method by mocking the page's evaluate method.
    This dummy page returns preset values for window.scrollY, window.innerHeight,
    and document.documentElement.scrollHeight. The test then verifies that the 
    computed scroll information (pixels_above and pixels_below) match the expected values.
    """
    # Define a dummy page with an async evaluate method returning preset values.
    class DummyPage:
        async def evaluate(self, script):
            if "window.scrollY" in script:
                return 100  # scrollY
            elif "window.innerHeight" in script:
                return 500  # innerHeight
            elif "document.documentElement.scrollHeight" in script:
                return 1200  # total scrollable height
            return None
    # Create a dummy session with a dummy current_page.
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = DummyPage()
    # We also need a dummy context attribute but it won't be used in this test.
    dummy_session.context = type("DummyContext", (), {})()
    # Create a dummy browser mock.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize BrowserContext with the dummy browser and config.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    # Manually set the session to our dummy session.
    context.session = dummy_session
    # Call get_scroll_info on the dummy page.
    pixels_above, pixels_below = await context.get_scroll_info(dummy_session.current_page)
    # Expected calculations:
    # pixels_above = scrollY = 100
    # pixels_below = total_height - (scrollY + innerHeight) = 1200 - (100 + 500) = 600
    assert pixels_above == 100, f"Expected 100 pixels above, got {pixels_above}"
    assert pixels_below == 600, f"Expected 600 pixels below, got {pixels_below}"
@pytest.mark.asyncio
async def test_reset_context():
    """
    Test the reset_context method to ensure it correctly closes all existing tabs,
    resets the cached state, and creates a new page.
    """
    # Dummy Page with close and wait_for_load_state methods.
    class DummyPage:
        def __init__(self, url="http://dummy.com"):
            self.url = url
            self.closed = False
        async def close(self):
            self.closed = True
        async def wait_for_load_state(self):
            pass
    # Dummy Context that holds pages and can create a new page.
    class DummyContext:
        def __init__(self):
            self.pages = []
        async def new_page(self):
            new_page = DummyPage(url="")
            self.pages.append(new_page)
            return new_page
    # Create a dummy session with a context containing two pages.
    dummy_session = type("DummySession", (), {})()
    dummy_context = DummyContext()
    page1 = DummyPage(url="http://page1.com")
    page2 = DummyPage(url="http://page2.com")
    dummy_context.pages.extend([page1, page2])
    dummy_session.context = dummy_context
    dummy_session.current_page = page1
    dummy_session.cached_state = None
    # Create a dummy browser mock.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize BrowserContext using our dummy_browser and config,
    # and manually set its session to our dummy session.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    context.session = dummy_session
    # Confirm session has 2 pages before reset.
    assert len(dummy_session.context.pages) == 2
    # Call reset_context which should close existing pages,
    # reset the cached state, and create a new page as current_page.
    await context.reset_context()
    # Verify that initial pages were closed.
    assert page1.closed is True
    assert page2.closed is True
    # Check that a new page is created and set as current_page.
    assert dummy_session.current_page is not None
    new_page = dummy_session.current_page
    # New page URL should be empty as per _get_initial_state.
    assert new_page.url == ""
    # Verify that cached_state is reset to an initial BrowserState.
    state = dummy_session.cached_state
    assert isinstance(state, BrowserState)
    assert state.url == ""
    assert state.element_tree.tag_name == 'root'
@pytest.mark.asyncio
async def test_take_screenshot():
    """
    Test the take_screenshot method to verify that it returns a base64 encoded screenshot string.
    A dummy page with a mocked screenshot method is used, returning a predefined byte string.
    """
    class DummyPage:
        async def screenshot(self, full_page, animations):
            # Verify that parameters are forwarded correctly.
            assert full_page is True, "full_page parameter was not correctly passed"
            assert animations == 'disabled', "animations parameter was not correctly passed"
            # Return a test byte string.
            return b'test'
    # Create a dummy session with the DummyPage as the current_page.
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = DummyPage()
    dummy_session.context = None  # Not used in this test
    # Create a dummy browser mock.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize the BrowserContext with the dummy browser and config.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    # Manually set the session to our dummy session.
    context.session = dummy_session
    # Call take_screenshot and check that it returns the expected base64 encoded string.
    result = await context.take_screenshot(full_page=True)
    expected = base64.b64encode(b'test').decode('utf-8')
    assert result == expected, f"Expected {expected}, but got {result}"
@pytest.mark.asyncio
async def test_refresh_page_behavior():
    """
    Test the refresh_page method of BrowserContext to verify that it correctly reloads the current page
    and waits for the page's load state. This is done by creating a dummy page that flags when its
    reload and wait_for_load_state methods are called.
    """
    class DummyPage:
        def __init__(self):
            self.reload_called = False
            self.wait_for_load_state_called = False
        async def reload(self):
            self.reload_called = True
        async def wait_for_load_state(self):
            self.wait_for_load_state_called = True
    # Create a dummy session with the dummy page as the current_page.
    dummy_page = DummyPage()
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = dummy_page
    dummy_session.context = None  # Not required for this test
    # Create a dummy browser mock
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize BrowserContext with the dummy browser and config,
    # and manually set its session to our dummy session.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    context.session = dummy_session
    # Call refresh_page and verify that reload and wait_for_load_state were called.
    await context.refresh_page()
    assert dummy_page.reload_called is True, "Expected the page to call reload()"
    assert dummy_page.wait_for_load_state_called is True, "Expected the page to call wait_for_load_state()"
@pytest.mark.asyncio
async def test_remove_highlights_failure():
    """
    Test the remove_highlights method to ensure that if the page.evaluate call fails,
    the exception is caught and does not propagate (i.e. the method handles errors gracefully).
    """
    # Dummy page that always raises an exception when evaluate is called.
    class DummyPage:
        async def evaluate(self, script):
            raise Exception("dummy error")
    # Create a dummy session with the DummyPage as current_page.
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = DummyPage()
    dummy_session.context = None  # Not used in this test
    # Create a dummy browser mock.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize BrowserContext with the dummy browser and configuration.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    context.session = dummy_session
    # Call remove_highlights and verify that no exception is raised.
    try:
        await context.remove_highlights()
    except Exception as e:
        pytest.fail(f"remove_highlights raised an exception: {e}")