/*
 * Copyright (c) 2026-present, the Ladybird developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Random.h>
#include <AK/ScopeGuard.h>
#include <LibCore/Directory.h>
#include <LibCore/StandardPaths.h>
#include <LibDatabase/Database.h>
#include <LibFileSystem/FileSystem.h>
#include <LibTest/TestCase.h>
#include <LibURL/Parser.h>
#include <LibWebView/HistoryStore.h>

static URL::URL parse_url(StringView url)
{
    auto parsed_url = URL::Parser::basic_parse(url);
    VERIFY(parsed_url.has_value());
    return parsed_url.release_value();
}

static void populate_history_for_url_autocomplete_tests(WebView::HistoryStore& store)
{
    store.record_visit(parse_url("https://www.google.com/"sv), {}, UnixDateTime::from_seconds_since_epoch(30));
    store.record_visit(parse_url("https://x.com/"sv), {}, UnixDateTime::from_seconds_since_epoch(20));
    store.record_visit(parse_url("https://github.com/LadybirdBrowser/ladybird"sv), {}, UnixDateTime::from_seconds_since_epoch(10));
}

static void expect_history_autocomplete_ignores_url_boilerplate(WebView::HistoryStore& store)
{
    populate_history_for_url_autocomplete_tests(store);

    EXPECT(store.autocomplete_entries("https://"sv, 8).is_empty());
    EXPECT(store.autocomplete_entries("https://www."sv, 8).is_empty());
    EXPECT(store.autocomplete_entries("www."sv, 8).is_empty());

    auto git_entries = store.autocomplete_entries("git"sv, 8);
    VERIFY(git_entries.size() == 1);
    EXPECT_EQ(git_entries[0].url, "https://github.com/LadybirdBrowser/ladybird"_string);

    auto https_goo_entries = store.autocomplete_entries("https://goo"sv, 8);
    VERIFY(https_goo_entries.size() == 1);
    EXPECT_EQ(https_goo_entries[0].url, "https://www.google.com/"_string);
}

static void expect_history_autocomplete_requires_three_characters_for_title_matches(WebView::HistoryStore& store)
{
    store.record_visit(parse_url("https://example.com/"sv), "Foo bar baz wip wap wop"_string, UnixDateTime::from_seconds_since_epoch(10));

    EXPECT(store.autocomplete_entries("w"sv, 8).is_empty());
    EXPECT(store.autocomplete_entries("wi"sv, 8).is_empty());

    auto entries = store.autocomplete_entries("wip"sv, 8);
    VERIFY(entries.size() == 1);
    EXPECT_EQ(entries[0].url, "https://example.com/"_string);
}

static void expect_history_autocomplete_requires_three_characters_for_non_prefix_url_matches(WebView::HistoryStore& store)
{
    store.record_visit(parse_url("https://example.com/wip-path"sv), "Example"_string, UnixDateTime::from_seconds_since_epoch(10));

    EXPECT(store.autocomplete_entries("w"sv, 8).is_empty());
    EXPECT(store.autocomplete_entries("wi"sv, 8).is_empty());

    auto entries = store.autocomplete_entries("wip"sv, 8);
    VERIFY(entries.size() == 1);
    EXPECT_EQ(entries[0].url, "https://example.com/wip-path"_string);
}

static void expect_history_autocomplete_entries_include_metadata(WebView::HistoryStore& store)
{
    auto google_url = parse_url("https://www.google.com/"sv);
    auto github_url = parse_url("https://github.com/LadybirdBrowser/ladybird"sv);

    store.record_visit(google_url, "Google"_string, UnixDateTime::from_seconds_since_epoch(20));
    store.update_favicon(google_url, "Zm9v"_string);
    store.record_visit(github_url, "Ladybird repository"_string, UnixDateTime::from_seconds_since_epoch(10));

    auto entries = store.autocomplete_entries("goo"sv, 8);
    VERIFY(entries.size() == 1);
    EXPECT_EQ(entries[0].url, "https://www.google.com/"_string);
    EXPECT_EQ(entries[0].title, Optional<String> { "Google"_string });
    EXPECT_EQ(entries[0].favicon_base64_png, Optional<String> { "Zm9v"_string });
    EXPECT_EQ(entries[0].visit_count, 1u);
    EXPECT_EQ(entries[0].last_visited_time, UnixDateTime::from_seconds_since_epoch(20));
}

TEST_CASE(record_and_lookup_history_entries)
{
    auto store = WebView::HistoryStore::create();

    auto visited_at = UnixDateTime::from_seconds_since_epoch(1234);
    store->record_visit(parse_url("https://example.com/path#fragment"sv), "Example page"_string, visited_at);
    store->record_visit(parse_url("https://example.com/path"sv), {}, visited_at);

    auto entry = store->entry_for_url(parse_url("https://example.com/path"sv));
    VERIFY(entry.has_value());

    EXPECT_EQ(entry->url, "https://example.com/path"_string);
    EXPECT_EQ(entry->title, Optional<String> { "Example page"_string });
    EXPECT_EQ(entry->visit_count, 2u);
    EXPECT_EQ(entry->last_visited_time, visited_at);
}

TEST_CASE(history_autocomplete_prefers_url_prefix_then_recency)
{
    auto store = WebView::HistoryStore::create();

    store->record_visit(parse_url("https://beta.example.com/"sv), "Alpha reference"_string, UnixDateTime::from_seconds_since_epoch(10));
    store->record_visit(parse_url("https://alpha.example.com/"sv), "Something else"_string, UnixDateTime::from_seconds_since_epoch(20));
    store->record_visit(parse_url("https://docs.example.com/"sv), "Alpha docs"_string, UnixDateTime::from_seconds_since_epoch(30));

    auto entries = store->autocomplete_entries("alpha"sv, 8);

    VERIFY(entries.size() == 3);
    EXPECT_EQ(entries[0].url, "https://alpha.example.com/"_string);
    EXPECT_EQ(entries[1].url, "https://docs.example.com/"_string);
    EXPECT_EQ(entries[2].url, "https://beta.example.com/"_string);
}

TEST_CASE(history_autocomplete_trims_whitespace)
{
    auto store = WebView::HistoryStore::create();

    store->record_visit(parse_url("https://ladybird.dev/"sv), "Ladybird"_string, UnixDateTime::from_seconds_since_epoch(10));

    auto entries = store->autocomplete_entries("  ladybird  "sv, 8);

    VERIFY(entries.size() == 1);
    EXPECT_EQ(entries[0].url, "https://ladybird.dev/"_string);
}

TEST_CASE(history_autocomplete_ignores_www_prefix_for_host_matches)
{
    auto store = WebView::HistoryStore::create();

    store->record_visit(parse_url("https://www.google.com/"sv), "Google"_string, UnixDateTime::from_seconds_since_epoch(20));
    store->record_visit(parse_url("https://www.goodreads.com/"sv), "Goodreads"_string, UnixDateTime::from_seconds_since_epoch(10));

    auto entries = store->autocomplete_entries("goo"sv, 8);

    VERIFY(entries.size() == 2);
    EXPECT_EQ(entries[0].url, "https://www.google.com/"_string);
    EXPECT_EQ(entries[1].url, "https://www.goodreads.com/"_string);
}

TEST_CASE(history_autocomplete_ignores_scheme_and_www_boilerplate_prefixes)
{
    auto store = WebView::HistoryStore::create();
    expect_history_autocomplete_ignores_url_boilerplate(*store);
}

TEST_CASE(history_autocomplete_requires_three_characters_for_title_matches)
{
    auto store = WebView::HistoryStore::create();
    expect_history_autocomplete_requires_three_characters_for_title_matches(*store);
}

TEST_CASE(history_autocomplete_requires_three_characters_for_non_prefix_url_matches)
{
    auto store = WebView::HistoryStore::create();
    expect_history_autocomplete_requires_three_characters_for_non_prefix_url_matches(*store);
}

TEST_CASE(history_favicon_updates_entry)
{
    auto store = WebView::HistoryStore::create();
    auto url = parse_url("https://ladybird.dev/"sv);

    store->record_visit(url, "Ladybird"_string, UnixDateTime::from_seconds_since_epoch(10));
    store->update_favicon(url, "Zm9v"_string);

    auto entry = store->entry_for_url(url);
    VERIFY(entry.has_value());
    EXPECT_EQ(entry->favicon_base64_png, Optional<String> { "Zm9v"_string });
}

TEST_CASE(recently_closed_entries_are_reopened_in_lifo_order)
{
    auto store = WebView::HistoryStore::create();
    auto first_url = parse_url("https://first.example.com/"sv);
    auto second_url = parse_url("https://second.example.com/"sv);
    auto third_url = parse_url("https://third.example.com/"sv);

    EXPECT(!store->has_recently_closed_entries());

    store->record_closed_tab(first_url, UnixDateTime::from_seconds_since_epoch(10));
    store->record_closed_window({ second_url, third_url }, 1, UnixDateTime::from_seconds_since_epoch(20));

    EXPECT(store->has_recently_closed_entries());

    auto recently_closed_entry = store->pop_most_recently_closed_entry();
    VERIFY(recently_closed_entry.has_value());
    EXPECT(recently_closed_entry->was_window);
    EXPECT_EQ(recently_closed_entry->active_tab_index, 1u);
    EXPECT_EQ(recently_closed_entry->urls.size(), 2u);
    EXPECT_EQ(recently_closed_entry->urls[0], second_url);
    EXPECT_EQ(recently_closed_entry->urls[1], third_url);
    EXPECT(store->has_recently_closed_entries());

    recently_closed_entry = store->pop_most_recently_closed_entry();
    VERIFY(recently_closed_entry.has_value());
    EXPECT(!recently_closed_entry->was_window);
    EXPECT_EQ(recently_closed_entry->active_tab_index, 0u);
    EXPECT_EQ(recently_closed_entry->urls.size(), 1u);
    EXPECT_EQ(recently_closed_entry->urls[0], first_url);
    EXPECT(!store->has_recently_closed_entries());

    EXPECT(!store->pop_most_recently_closed_entry().has_value());
}

TEST_CASE(recently_closed_entries_accessed_since_can_be_removed)
{
    auto store = WebView::HistoryStore::create();
    auto older_url = parse_url("https://older.example.com/"sv);
    auto newer_url = parse_url("https://newer.example.com/"sv);
    auto newest_url = parse_url("https://newest.example.com/"sv);

    store->record_closed_tab(older_url, UnixDateTime::from_seconds_since_epoch(10));
    store->record_closed_window({ newer_url, newest_url }, 0, UnixDateTime::from_seconds_since_epoch(20));

    store->remove_entries_accessed_since(UnixDateTime::from_seconds_since_epoch(15));

    EXPECT(store->has_recently_closed_entries());

    auto recently_closed_entry = store->pop_most_recently_closed_entry();
    VERIFY(recently_closed_entry.has_value());
    EXPECT_EQ(recently_closed_entry->urls.size(), 1u);
    EXPECT_EQ(recently_closed_entry->urls[0], older_url);
    EXPECT(!store->has_recently_closed_entries());
}

TEST_CASE(recently_closed_entries_can_be_peeked_without_popping)
{
    auto store = WebView::HistoryStore::create();
    auto first_url = parse_url("https://peek.example.com/"sv);
    auto second_url = parse_url("https://peek-two.example.com/"sv);

    store->record_closed_window({ first_url, second_url }, 0, UnixDateTime::from_seconds_since_epoch(10));

    auto recently_closed_entry = store->most_recently_closed_entry();
    VERIFY(recently_closed_entry.has_value());
    EXPECT(recently_closed_entry->was_window);
    EXPECT_EQ(recently_closed_entry->urls.size(), 2u);
    EXPECT_EQ(recently_closed_entry->urls[0], first_url);
    EXPECT_EQ(recently_closed_entry->urls[1], second_url);
    EXPECT(store->has_recently_closed_entries());
}

TEST_CASE(recently_closed_entries_are_cleared_with_history)
{
    auto store = WebView::HistoryStore::create();
    auto first_url = parse_url("https://clear.example.com/"sv);
    auto second_url = parse_url("https://clear-two.example.com/"sv);

    store->record_closed_tab(first_url, UnixDateTime::from_seconds_since_epoch(10));
    store->record_closed_window({ second_url }, 0, UnixDateTime::from_seconds_since_epoch(20));

    store->clear();

    EXPECT(!store->has_recently_closed_entries());
    EXPECT(!store->most_recently_closed_entry().has_value());
    EXPECT(!store->pop_most_recently_closed_entry().has_value());
}

TEST_CASE(history_autocomplete_entries_include_metadata)
{
    auto store = WebView::HistoryStore::create();
    expect_history_autocomplete_entries_include_metadata(*store);
}

TEST_CASE(non_browsable_urls_are_not_recorded)
{
    auto store = WebView::HistoryStore::create();

    store->record_visit(parse_url("about:blank"sv));
    store->record_visit(parse_url("data:text/plain,hello"sv));

    EXPECT(!store->entry_for_url(parse_url("about:blank"sv)).has_value());
    EXPECT(!store->entry_for_url(parse_url("data:text/plain,hello"sv)).has_value());
}

TEST_CASE(disabled_history_store_ignores_updates)
{
    auto store = WebView::HistoryStore::create_disabled();
    auto url = parse_url("https://example.com/"sv);

    auto check_is_empty = [&] {
        EXPECT(!store->entry_for_url(url).has_value());
        EXPECT(store->autocomplete_entries("example"sv, 8).is_empty());
    };

    store->record_visit(url, "Example"_string, UnixDateTime::from_seconds_since_epoch(10));
    check_is_empty();

    store->update_title(url, "Example title"_string);
    check_is_empty();

    store->remove_entries_accessed_since(UnixDateTime::from_seconds_since_epoch(0));
    check_is_empty();

    store->clear();
    check_is_empty();
}

TEST_CASE(history_entries_accessed_since_can_be_removed)
{
    auto store = WebView::HistoryStore::create();

    auto older_url = parse_url("https://older.example.com/"sv);
    auto newer_url = parse_url("https://newer.example.com/"sv);

    store->record_visit(older_url, "Older"_string, UnixDateTime::from_seconds_since_epoch(10));
    store->record_visit(newer_url, "Newer"_string, UnixDateTime::from_seconds_since_epoch(20));

    store->remove_entries_accessed_since(UnixDateTime::from_seconds_since_epoch(15));

    EXPECT(store->entry_for_url(older_url).has_value());
    EXPECT(!store->entry_for_url(newer_url).has_value());
}

TEST_CASE(persisted_history_survives_reopen)
{
    auto database_directory = ByteString::formatted(
        "{}/ladybird-history-store-test-{}",
        Core::StandardPaths::tempfile_directory(),
        generate_random_uuid());
    TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes));

    auto cleanup = ScopeGuard([&] {
        MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed));
    });

    {
        auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv));
        auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database));
        store->record_visit(parse_url("https://persist.example.com/"sv), "Persisted title"_string, UnixDateTime::from_seconds_since_epoch(77));
        store->update_favicon(parse_url("https://persist.example.com/"sv), "Zm9v"_string);
    }

    {
        auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv));
        auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database));

        auto entry = store->entry_for_url(parse_url("https://persist.example.com/"sv));
        VERIFY(entry.has_value());

        EXPECT_EQ(entry->title, Optional<String> { "Persisted title"_string });
        EXPECT_EQ(entry->visit_count, 1u);
        EXPECT_EQ(entry->last_visited_time, UnixDateTime::from_seconds_since_epoch(77));
        EXPECT_EQ(entry->favicon_base64_png, Optional<String> { "Zm9v"_string });
    }
}

TEST_CASE(persisted_history_entries_accessed_since_can_be_removed)
{
    auto database_directory = ByteString::formatted(
        "{}/ladybird-history-store-remove-since-test-{}",
        Core::StandardPaths::tempfile_directory(),
        generate_random_uuid());
    TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes));

    auto cleanup = ScopeGuard([&] {
        MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed));
    });

    auto older_url = parse_url("https://older.example.com/"sv);
    auto newer_url = parse_url("https://newer.example.com/"sv);

    {
        auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv));
        auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database));
        store->record_visit(older_url, "Older"_string, UnixDateTime::from_seconds_since_epoch(10));
        store->record_visit(newer_url, "Newer"_string, UnixDateTime::from_seconds_since_epoch(20));
        store->remove_entries_accessed_since(UnixDateTime::from_seconds_since_epoch(15));
    }

    {
        auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv));
        auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database));

        EXPECT(store->entry_for_url(older_url).has_value());
        EXPECT(!store->entry_for_url(newer_url).has_value());
    }
}

TEST_CASE(persisted_history_autocomplete_ignores_scheme_and_www_boilerplate_prefixes)
{
    auto database_directory = ByteString::formatted(
        "{}/ladybird-history-store-autocomplete-test-{}",
        Core::StandardPaths::tempfile_directory(),
        generate_random_uuid());
    TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes));

    auto cleanup = ScopeGuard([&] {
        MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed));
    });

    auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv));
    auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database));

    expect_history_autocomplete_ignores_url_boilerplate(*store);
}

TEST_CASE(persisted_history_autocomplete_requires_three_characters_for_title_matches)
{
    auto database_directory = ByteString::formatted(
        "{}/ladybird-history-store-title-autocomplete-test-{}",
        Core::StandardPaths::tempfile_directory(),
        generate_random_uuid());
    TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes));

    auto cleanup = ScopeGuard([&] {
        MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed));
    });

    auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv));
    auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database));

    expect_history_autocomplete_requires_three_characters_for_title_matches(*store);
}

TEST_CASE(persisted_history_autocomplete_requires_three_characters_for_non_prefix_url_matches)
{
    auto database_directory = ByteString::formatted(
        "{}/ladybird-history-store-url-autocomplete-test-{}",
        Core::StandardPaths::tempfile_directory(),
        generate_random_uuid());
    TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes));

    auto cleanup = ScopeGuard([&] {
        MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed));
    });

    auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv));
    auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database));

    expect_history_autocomplete_requires_three_characters_for_non_prefix_url_matches(*store);
}

TEST_CASE(persisted_history_autocomplete_entries_include_metadata)
{
    auto database_directory = ByteString::formatted(
        "{}/ladybird-history-store-entry-autocomplete-test-{}",
        Core::StandardPaths::tempfile_directory(),
        generate_random_uuid());
    TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes));

    auto cleanup = ScopeGuard([&] {
        MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed));
    });

    auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv));
    auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database));

    expect_history_autocomplete_entries_include_metadata(*store);
}
