# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import os
import re
import zipfile
from pathlib import Path
from typing import Any, cast

import pytest

from playwright.sync_api import Browser, BrowserContext, Error, Page, Route, expect
from tests.server import Server


def test_should_work(browser: Browser, server: Server, tmp_path: Path) -> None:
    path = os.path.join(tmp_path, "log.har")
    context = browser.new_context(record_har_path=path)
    page = context.new_page()
    page.goto(server.EMPTY_PAGE)
    context.close()
    with open(path) as f:
        data = json.load(f)
        assert "log" in data


def test_should_omit_content(browser: Browser, server: Server, tmp_path: Path) -> None:
    path = os.path.join(tmp_path, "log.har")
    context = browser.new_context(record_har_path=path, record_har_content="omit")
    page = context.new_page()
    page.goto(server.PREFIX + "/har.html")
    context.close()
    with open(path) as f:
        data = json.load(f)
        assert "log" in data
        log = data["log"]

        content1 = log["entries"][0]["response"]["content"]
        assert "text" not in content1
        assert "encoding" not in content1


def test_should_omit_content_legacy(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    path = os.path.join(tmp_path, "log.har")
    context = browser.new_context(record_har_path=path, record_har_omit_content=True)
    page = context.new_page()
    page.goto(server.PREFIX + "/har.html")
    context.close()
    with open(path) as f:
        data = json.load(f)
        assert "log" in data
        log = data["log"]

        content1 = log["entries"][0]["response"]["content"]
        assert "text" not in content1
        assert "encoding" not in content1


def test_should_attach_content(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    path = os.path.join(tmp_path, "log.har.zip")
    context = browser.new_context(
        record_har_path=path,
        record_har_content="attach",
    )
    page = context.new_page()
    page.goto(server.PREFIX + "/har.html")
    page.evaluate("() => fetch('/pptr.png').then(r => r.arrayBuffer())")
    context.close()
    with zipfile.ZipFile(path) as z:
        with z.open("har.har") as har:
            entries = json.load(har)["log"]["entries"]

            assert "encoding" not in entries[0]["response"]["content"]
            assert (
                entries[0]["response"]["content"]["mimeType"]
                == "text/html; charset=utf-8"
            )
            assert (
                "75841480e2606c03389077304342fac2c58ccb1b"
                in entries[0]["response"]["content"]["_file"]
            )
            assert entries[0]["response"]["content"]["size"] >= 96
            assert entries[0]["response"]["content"]["compression"] == 0

            assert "encoding" not in entries[1]["response"]["content"]
            assert (
                entries[1]["response"]["content"]["mimeType"]
                == "text/css; charset=utf-8"
            )
            assert (
                "79f739d7bc88e80f55b9891a22bf13a2b4e18adb"
                in entries[1]["response"]["content"]["_file"]
            )
            assert entries[1]["response"]["content"]["size"] >= 37
            assert entries[1]["response"]["content"]["compression"] == 0

            assert "encoding" not in entries[2]["response"]["content"]
            assert entries[2]["response"]["content"]["mimeType"] == "image/png"
            assert (
                "a4c3a18f0bb83f5d9fe7ce561e065c36205762fa"
                in entries[2]["response"]["content"]["_file"]
            )
            assert entries[2]["response"]["content"]["size"] >= 6000
            assert entries[2]["response"]["content"]["compression"] == 0

            with z.open("75841480e2606c03389077304342fac2c58ccb1b.html") as f:
                assert b"HAR Page" in f.read()

            with z.open("79f739d7bc88e80f55b9891a22bf13a2b4e18adb.css") as f:
                assert b"pink" in f.read()

            with z.open("a4c3a18f0bb83f5d9fe7ce561e065c36205762fa.png") as f:
                assert len(f.read()) == entries[2]["response"]["content"]["size"]


def test_should_include_content(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    path = os.path.join(tmp_path, "log.har")
    context = browser.new_context(record_har_path=path)
    page = context.new_page()
    page.goto(server.PREFIX + "/har.html")
    context.close()
    with open(path) as f:
        data = json.load(f)
        assert "log" in data
        log = data["log"]

        content1 = log["entries"][0]["response"]["content"]
        assert content1["mimeType"] == "text/html; charset=utf-8"
        assert "HAR Page" in content1["text"]


def test_should_default_to_full_mode(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    path = os.path.join(tmp_path, "log.har")
    context = browser.new_context(
        record_har_path=path,
    )
    page = context.new_page()
    page.goto(server.PREFIX + "/har.html")
    context.close()
    with open(path) as f:
        data = json.load(f)
        assert "log" in data
        log = data["log"]
        assert log["entries"][0]["request"]["bodySize"] >= 0


def test_should_support_minimal_mode(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    path = os.path.join(tmp_path, "log.har")
    context = browser.new_context(
        record_har_path=path,
        record_har_mode="minimal",
    )
    page = context.new_page()
    page.goto(server.PREFIX + "/har.html")
    context.close()
    with open(path) as f:
        data = json.load(f)
        assert "log" in data
        log = data["log"]
        assert log["entries"][0]["request"]["bodySize"] == -1


def test_should_filter_by_glob(browser: Browser, server: Server, tmp_path: str) -> None:
    path = os.path.join(tmp_path, "log.har")
    context = browser.new_context(
        base_url=server.PREFIX,
        record_har_path=path,
        record_har_url_filter="/*.css",
        ignore_https_errors=True,
    )
    page = context.new_page()
    page.goto(server.PREFIX + "/har.html")
    context.close()
    with open(path) as f:
        data = json.load(f)
        assert "log" in data
        log = data["log"]
        assert len(log["entries"]) == 1
        assert log["entries"][0]["request"]["url"].endswith("one-style.css")


def test_should_filter_by_regexp(
    browser: Browser, server: Server, tmp_path: str
) -> None:
    path = os.path.join(tmp_path, "log.har")
    context = browser.new_context(
        base_url=server.PREFIX,
        record_har_path=path,
        record_har_url_filter=re.compile("HAR.X?HTML", re.I),
        ignore_https_errors=True,
    )
    page = context.new_page()
    page.goto(server.PREFIX + "/har.html")
    context.close()
    with open(path) as f:
        data = json.load(f)
        assert "log" in data
        log = data["log"]
        assert len(log["entries"]) == 1
        assert log["entries"][0]["request"]["url"].endswith("har.html")


def test_should_context_route_from_har_matching_the_method_and_following_redirects(
    context: BrowserContext, assetdir: Path
) -> None:
    context.route_from_har(har=assetdir / "har-fulfill.har")
    page = context.new_page()
    page.goto("http://no.playwright/")
    # HAR contains a redirect for the script that should be followed automatically.
    assert page.evaluate("window.value") == "foo"
    # HAR contains a POST for the css file that should not be used.
    expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)")


def test_should_page_route_from_har_matching_the_method_and_following_redirects(
    page: Page, assetdir: Path
) -> None:
    page.route_from_har(har=assetdir / "har-fulfill.har")
    page.goto("http://no.playwright/")
    # HAR contains a redirect for the script that should be followed automatically.
    assert page.evaluate("window.value") == "foo"
    # HAR contains a POST for the css file that should not be used.
    expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)")


def test_fallback_continue_should_continue_when_not_found_in_har(
    context: BrowserContext, server: Server, assetdir: Path
) -> None:
    context.route_from_har(har=assetdir / "har-fulfill.har", not_found="fallback")
    page = context.new_page()
    page.goto(server.PREFIX + "/one-style.html")
    expect(page.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")


def test_by_default_should_abort_requests_not_found_in_har(
    context: BrowserContext,
    server: Server,
    assetdir: Path,
    is_chromium: bool,
    is_webkit: bool,
) -> None:
    context.route_from_har(har=assetdir / "har-fulfill.har")
    page = context.new_page()

    with pytest.raises(Error) as exc_info:
        page.goto(server.EMPTY_PAGE)
    assert exc_info.value
    if is_chromium:
        assert "net::ERR_FAILED" in exc_info.value.message
    elif is_webkit:
        assert "Blocked by Web Inspector" in exc_info.value.message
    else:
        assert "NS_ERROR_FAILURE" in exc_info.value.message


def test_fallback_continue_should_continue_requests_on_bad_har(
    context: BrowserContext, server: Server, tmp_path: Path
) -> None:
    path_to_invalid_har = tmp_path / "invalid.har"
    with path_to_invalid_har.open("w") as f:
        json.dump({"log": {}}, f)
    context.route_from_har(har=path_to_invalid_har, not_found="fallback")
    page = context.new_page()
    page.goto(server.PREFIX + "/one-style.html")
    expect(page.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")


def test_should_only_handle_requests_matching_url_filter(
    context: BrowserContext, server: Server, assetdir: Path
) -> None:
    context.route_from_har(
        har=assetdir / "har-fulfill.har", not_found="fallback", url="**/*.js"
    )
    page = context.new_page()

    def handler(route: Route) -> None:
        assert route.request.url == "http://no.playwright/"
        route.fulfill(
            status=200,
            content_type="text/html",
            body='<script src="./script.js"></script><div>hello</div>',
        )

    context.route("http://no.playwright/", handler)
    page.goto("http://no.playwright/")
    assert page.evaluate("window.value") == "foo"
    expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)")


def test_should_only_handle_requests_matching_url_filter_no_fallback(
    context: BrowserContext, server: Server, assetdir: Path
) -> None:
    context.route_from_har(har=assetdir / "har-fulfill.har", url="**/*.js")
    page = context.new_page()

    def handler(route: Route) -> None:
        assert route.request.url == "http://no.playwright/"
        route.fulfill(
            status=200,
            content_type="text/html",
            body='<script src="./script.js"></script><div>hello</div>',
        )

    context.route("http://no.playwright/", handler)
    page.goto("http://no.playwright/")
    assert page.evaluate("window.value") == "foo"
    expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)")


def test_should_only_handle_requests_matching_url_filter_no_fallback_page(
    page: Page, server: Server, assetdir: Path
) -> None:
    page.route_from_har(har=assetdir / "har-fulfill.har", url="**/*.js")

    def handler(route: Route) -> None:
        assert route.request.url == "http://no.playwright/"
        route.fulfill(
            status=200,
            content_type="text/html",
            body='<script src="./script.js"></script><div>hello</div>',
        )

    page.route("http://no.playwright/", handler)
    page.goto("http://no.playwright/")
    assert page.evaluate("window.value") == "foo"
    expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)")


def test_should_support_regex_filter(
    context: BrowserContext, server: Server, assetdir: Path
) -> None:
    context.route_from_har(
        har=assetdir / "har-fulfill.har",
        url=re.compile(r".*(\.js|.*\.css|no.playwright\/)"),
    )
    page = context.new_page()
    page.goto("http://no.playwright/")
    assert page.evaluate("window.value") == "foo"
    expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)")


def test_should_go_back_to_redirected_navigation(
    context: BrowserContext, server: Server, assetdir: Path
) -> None:
    context.route_from_har(
        har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/")
    )
    page = context.new_page()
    page.goto("https://theverge.com/")
    page.goto(server.EMPTY_PAGE)
    expect(page).to_have_url(server.EMPTY_PAGE)

    response = page.go_back()
    assert response
    expect(page).to_have_url("https://www.theverge.com/")
    assert response.request.url == "https://www.theverge.com/"
    assert page.evaluate("window.location.href") == "https://www.theverge.com/"


@pytest.mark.skip_browser(
    "firefox"
)  # skipped upstream (https://github.com/microsoft/playwright/blob/6a8d835145e2f4002ee00b67a80a1f70af956703/tests/library/browsercontext-har.spec.ts#L214)
def test_should_go_forward_to_redirected_navigation(
    context: BrowserContext, server: Server, assetdir: Path
) -> None:
    context.route_from_har(
        har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/")
    )
    page = context.new_page()
    page.goto("https://theverge.com/")
    page.goto(server.EMPTY_PAGE)
    expect(page).to_have_url(server.EMPTY_PAGE)
    page.goto("https://theverge.com/")
    expect(page).to_have_url("https://www.theverge.com/")
    page.go_back()
    expect(page).to_have_url(server.EMPTY_PAGE)
    response = page.go_forward()
    assert response
    expect(page).to_have_url("https://www.theverge.com/")
    assert response.request.url == "https://www.theverge.com/"
    assert page.evaluate("window.location.href") == "https://www.theverge.com/"


def test_should_reload_redirected_navigation(
    context: BrowserContext, server: Server, assetdir: Path
) -> None:
    context.route_from_har(
        har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/")
    )
    page = context.new_page()
    page.goto("https://theverge.com/")
    expect(page).to_have_url("https://www.theverge.com/")
    response = page.reload()
    assert response
    expect(page).to_have_url("https://www.theverge.com/")
    assert response.request.url == "https://www.theverge.com/"
    assert page.evaluate("window.location.href") == "https://www.theverge.com/"


def test_should_fulfill_from_har_with_content_in_a_file(
    context: BrowserContext, server: Server, assetdir: Path
) -> None:
    context.route_from_har(har=assetdir / "har-sha1.har")
    page = context.new_page()
    page.goto("http://no.playwright/")
    assert page.content() == "<html><head></head><body>Hello, world</body></html>"


def test_should_round_trip_har_zip(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    har_path = tmp_path / "har.zip"
    context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path)
    page_1 = context_1.new_page()
    page_1.goto(server.PREFIX + "/one-style.html")
    context_1.close()

    context_2 = browser.new_context()
    context_2.route_from_har(har=har_path, not_found="abort")
    page_2 = context_2.new_page()
    page_2.goto(server.PREFIX + "/one-style.html")
    assert "hello, world!" in page_2.content()
    expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
    context_2.close()


def test_should_round_trip_har_with_post_data(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    server.set_route(
        "/echo", lambda req: (req.write(cast(Any, req).post_body), req.finish())
    )
    fetch_function = """
        async (body) => {
            const response = await fetch('/echo', { method: 'POST', body });
            return response.text();
        };
    """
    har_path = tmp_path / "har.zip"
    context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path)
    page_1 = context_1.new_page()
    page_1.goto(server.EMPTY_PAGE)

    assert page_1.evaluate(fetch_function, "1") == "1"
    assert page_1.evaluate(fetch_function, "2") == "2"
    assert page_1.evaluate(fetch_function, "3") == "3"
    context_1.close()

    context_2 = browser.new_context()
    context_2.route_from_har(har=har_path, not_found="abort")
    page_2 = context_2.new_page()
    page_2.goto(server.EMPTY_PAGE)
    assert page_2.evaluate(fetch_function, "1") == "1"
    assert page_2.evaluate(fetch_function, "2") == "2"
    assert page_2.evaluate(fetch_function, "3") == "3"
    with pytest.raises(Exception):
        page_2.evaluate(fetch_function, "4")
    context_2.close()


def test_should_disambiguate_by_header(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    server.set_route(
        "/echo",
        lambda req: (req.write(cast(str, req.getHeader("baz")).encode()), req.finish()),
    )
    fetch_function = """
        async (bazValue) => {
            const response = await fetch('/echo', {
            method: 'POST',
            body: '',
            headers: {
                foo: 'foo-value',
                bar: 'bar-value',
                baz: bazValue,
            }
            });
            return response.text();
        };
    """
    har_path = tmp_path / "har.zip"
    context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path)
    page_1 = context_1.new_page()
    page_1.goto(server.EMPTY_PAGE)

    assert page_1.evaluate(fetch_function, "baz1") == "baz1"
    assert page_1.evaluate(fetch_function, "baz2") == "baz2"
    assert page_1.evaluate(fetch_function, "baz3") == "baz3"
    context_1.close()

    context_2 = browser.new_context()
    context_2.route_from_har(har=har_path)
    page_2 = context_2.new_page()
    page_2.goto(server.EMPTY_PAGE)
    assert page_2.evaluate(fetch_function, "baz1") == "baz1"
    assert page_2.evaluate(fetch_function, "baz2") == "baz2"
    assert page_2.evaluate(fetch_function, "baz3") == "baz3"
    assert page_2.evaluate(fetch_function, "baz4") == "baz1"
    context_2.close()


def test_should_produce_extracted_zip(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    har_path = tmp_path / "har.har"
    context = browser.new_context(
        record_har_mode="minimal", record_har_path=har_path, record_har_content="attach"
    )
    page_1 = context.new_page()
    page_1.goto(server.PREFIX + "/one-style.html")
    context.close()

    assert har_path.exists()
    with har_path.open() as r:
        content = r.read()
        assert "log" in content
        assert "background-color" not in r.read()

    context_2 = browser.new_context()
    context_2.route_from_har(har_path, not_found="abort")
    page_2 = context_2.new_page()
    page_2.goto(server.PREFIX + "/one-style.html")
    assert "hello, world!" in page_2.content()
    expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
    context_2.close()


def test_should_update_har_zip_for_context(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    har_path = tmp_path / "har.zip"
    context = browser.new_context()
    context.route_from_har(har_path, update=True)
    page_1 = context.new_page()
    page_1.goto(server.PREFIX + "/one-style.html")
    context.close()

    assert har_path.exists()

    context_2 = browser.new_context()
    context_2.route_from_har(har_path, not_found="abort")
    page_2 = context_2.new_page()
    page_2.goto(server.PREFIX + "/one-style.html")
    assert "hello, world!" in page_2.content()
    expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
    context_2.close()


def test_should_update_har_zip_for_page(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    har_path = tmp_path / "har.zip"
    context = browser.new_context()
    page_1 = context.new_page()
    page_1.route_from_har(har_path, update=True)
    page_1.goto(server.PREFIX + "/one-style.html")
    context.close()

    assert har_path.exists()

    context_2 = browser.new_context()
    page_2 = context_2.new_page()
    page_2.route_from_har(har_path, not_found="abort")
    page_2.goto(server.PREFIX + "/one-style.html")
    assert "hello, world!" in page_2.content()
    expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
    context_2.close()


def test_should_update_har_zip_for_page_with_different_options(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    har_path = tmp_path / "har.zip"
    context1 = browser.new_context()
    page1 = context1.new_page()
    page1.route_from_har(
        har_path, update=True, update_content="embed", update_mode="full"
    )
    page1.goto(server.PREFIX + "/one-style.html")
    context1.close()

    context2 = browser.new_context()
    page2 = context2.new_page()
    page2.route_from_har(har_path, not_found="abort")
    page2.goto(server.PREFIX + "/one-style.html")
    assert "hello, world!" in page2.content()
    expect(page2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
    context2.close()


def test_should_update_extracted_har_zip_for_page(
    browser: Browser, server: Server, tmp_path: Path
) -> None:
    har_path = tmp_path / "har.har"
    context = browser.new_context()
    page_1 = context.new_page()
    page_1.route_from_har(har_path, update=True)
    page_1.goto(server.PREFIX + "/one-style.html")
    context.close()

    assert har_path.exists()
    with har_path.open() as r:
        content = r.read()
        assert "log" in content
        assert "background-color" not in r.read()

    context_2 = browser.new_context()
    page_2 = context_2.new_page()
    page_2.route_from_har(har_path, not_found="abort")
    page_2.goto(server.PREFIX + "/one-style.html")
    assert "hello, world!" in page_2.content()
    expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
    context_2.close()
