/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.ui.navigation

import com.runicgateway.app.data.repository.ContentRepository.PostCategory
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * The website path → app route table (THEMING_AND_NAV.md §6.2).
 *
 * This is the milestone's one piece of cross-repo coupling, so the tests are
 * mostly about the table's *shape* — that it stays complete, unambiguous, and
 * honest about which rows the app actually surfaces in its drawer.
 */
class NavPathsTest {

    @Test fun everyWebsiteNavPathIsMapped() {
        // The sixteen rows of SiteHeader.jsx's NAV, quoted in NavPaths.kt. If the
        // site adds one, this is the test that says so — a path with no mapping is
        // silently unresolvable in phase 6's link handling.
        assertEquals(16, WEBSITE_PUBLIC_NAV.size)
        assertEquals(WEBSITE_PUBLIC_NAV.size, WEB_PATH_TO_ROUTE.size)
    }

    @Test fun everyMappedRouteIsDistinct() {
        // WEB_ROUTE_ORDER is keyed by route, so a duplicate would silently drop a
        // row's position from the sort.
        assertEquals(WEBSITE_PUBLIC_NAV.size, WEBSITE_PUBLIC_NAV.map { it.route }.toSet().size)
        assertEquals(WEBSITE_PUBLIC_NAV.size, WEB_ROUTE_ORDER.size)
    }

    @Test fun theWebsitesOrderIsPreserved() {
        // Load-bearing: a stored `order` is an index into this list.
        assertEquals(0, WEB_ROUTE_ORDER[Routes.HOME])
        assertEquals(1, WEB_ROUTE_ORDER[Routes.NEWS])
        assertEquals(5, WEB_ROUTE_ORDER[Routes.WIKI])
        assertEquals(15, WEB_ROUTE_ORDER[Routes.page("about")])
    }

    @Test fun theNineDrawerRowsAreTheIntersectionWithAppMenu() {
        // Nine of the sixteen have a drawer row. The other seven are mapped but not
        // surfaced — three news category tabs and the four Shard hub boards — and
        // an override for one of them is ignored rather than obeyed (§6.2).
        val coded = APP_MENU.map { it.route }.toSet()
        val surfaced = WEBSITE_PUBLIC_NAV.filter { it.route in coded }.map { it.path }

        assertEquals(
            listOf(
                "/", "/site/news", "/wiki", "/site/shard", "/site/rules",
                "/site/atlas", "/site/leaderboards", "/site/market", "/site/about",
            ),
            surfaced,
        )
    }

    @Test fun theSevenUnsurfacedPathsStillResolveToAScreen() {
        // Phase 6's added links resolve against the same table, and there a category
        // tab or a hub board is a perfectly good destination.
        val unsurfaced = listOf(
            "/site/screenshots", "/site/five-on-friday", "/site/newsletter",
            "/site/champs", "/site/guilds", "/site/governors", "/site/houses",
        )

        assertTrue(unsurfaced.all { appRouteForWebPath(it) != null })
        assertTrue(unsurfaced.none { appRouteForWebPath(it) in APP_MENU.map { e -> e.route } })
    }

    @Test fun theNewsCategoriesMapToTheirTab() {
        assertEquals("news?category=screenshots", appRouteForWebPath("/site/screenshots"))
        assertEquals("news?category=five-on-friday", appRouteForWebPath("/site/five-on-friday"))
        assertEquals("news?category=newsletter", appRouteForWebPath("/site/newsletter"))
        // The plain news path is the un-argumented route, so it matches the drawer's
        // coded row and opens the default tab.
        assertEquals(Routes.NEWS, appRouteForWebPath("/site/news"))
    }

    @Test fun theCategoryRouteMatchesTheNavHostPattern() {
        // The pattern the NavHost declares and the value callers navigate to have to
        // agree on the query key, or the argument arrives as null and the screen
        // silently opens the default tab.
        assertEquals("news?category={category}", Routes.NEWS_ROUTE)
        assertTrue(Routes.NEWS_ROUTE.startsWith("${Routes.NEWS}?"))
        for (category in PostCategory.entries) {
            assertEquals("${Routes.NEWS}?category=${category.urlSlug}", Routes.news(category))
        }
    }

    @Test fun theRoutePatternStripsToTheTopLevelRoute() {
        // How RunicApp recognizes the News destination: `destination.route` is the
        // pattern, and the drawer's row is the bare route.
        assertEquals(Routes.NEWS, Routes.NEWS_ROUTE.substringBefore('?'))
        assertEquals(Routes.NEWS, Routes.news(PostCategory.NEWSLETTER).substringBefore('?'))
    }

    // ── Lookup hygiene ───────────────────────────────────────────────────

    @Test fun anUnknownPathResolvesToNothing() {
        assertNull(appRouteForWebPath("/admin/appearance"))
        assertNull(appRouteForWebPath("/site/news/some-post"))
        assertNull(appRouteForWebPath("https://elsewhere.example/"))
    }

    @Test fun blankAndNullResolveToNothing() {
        assertNull(appRouteForWebPath(null))
        assertNull(appRouteForWebPath(""))
        assertNull(appRouteForWebPath("   "))
    }

    @Test fun aTrailingSlashIsTolerated() {
        // A hand-edited settings row may carry one; the root is left alone.
        assertEquals(Routes.WIKI, appRouteForWebPath("/wiki/"))
        assertEquals(Routes.SHARD, appRouteForWebPath("  /site/shard/  "))
        assertEquals(Routes.HOME, appRouteForWebPath("/"))
    }

    // ── resolveWebPath: an added link may name any page on the site (§6.3) ──

    @Test fun theNavTablesSixteenPathsResolveTheSameWay() {
        // An added link to a path the nav already knows must land where the nav row
        // does, or the same destination would behave differently depending on how
        // the admin reached it.
        for (row in WEBSITE_PUBLIC_NAV) {
            assertEquals(row.route, resolveWebPath(row.path))
        }
    }

    @Test fun theSitesDetailRoutesResolve() {
        // Read off website/client/src/App.jsx. Note what is NOT here: the site has
        // no /site/news/<id> route — its one post-detail route is the newsletter's.
        assertEquals(Routes.wikiPage("smithing"), resolveWebPath("/wiki/smithing"))
        assertEquals(Routes.atlasCreature("dragon"), resolveWebPath("/site/atlas/dragon"))
        assertEquals(Routes.marketVendor("0x24C"), resolveWebPath("/site/market/vendors/0x24C"))
        assertEquals(Routes.post("newsletter", "12"), resolveWebPath("/site/newsletter/12"))
    }

    @Test fun aTopLevelSlugIsACmsPage() {
        // The site serves CMS pages from a top-level /<slug>, so this is the rule
        // that opens an admin's own page natively rather than in a browser.
        assertEquals(Routes.page("donate"), resolveWebPath("/donate"))
        assertEquals(Routes.page("about"), resolveWebPath("/site/about"))
    }

    @Test fun theSitesOwnSectionsAreNotCmsPages() {
        // React Router ranks its static routes above /:slug, and so must the app —
        // otherwise a link to the admin panel would open a 404 CMS page in-app
        // instead of the real thing in a browser.
        for (path in listOf("/admin", "/account", "/player", "/site", "/invite", "/preview", "/api", "/uploads")) {
            assertNull(path, resolveWebPath(path))
        }
        // /wiki is reserved from the catch-all but mapped by the table above it.
        assertEquals(Routes.WIKI, resolveWebPath("/wiki"))
    }

    @Test fun aPathTheAppHasNoScreenForHandsOff() {
        assertNull(resolveWebPath("/site/status"))
        assertNull(resolveWebPath("/site/shard/activity"))
        assertNull(resolveWebPath("/account/login"))
        assertNull(resolveWebPath("/admin/navigation"))
        assertNull(resolveWebPath("/site/atlas/dragon/extra"))
    }

    @Test fun aQueryOrFragmentHandsOff() {
        // No app route takes either, so a native match would quietly drop what the
        // admin wrote. The browser honors it exactly.
        assertNull(resolveWebPath("/site/news?tag=patch"))
        assertNull(resolveWebPath("/donate#tiers"))
        assertEquals(Routes.NEWS, resolveWebPath("/site/news"))
    }

    @Test fun aMalformedPathResolvesToNothing() {
        assertNull(resolveWebPath(null))
        assertNull(resolveWebPath(""))
        assertNull(resolveWebPath("/site//news"))
        assertNull(resolveWebPath("https://elsewhere.example/donate"))
    }
}
