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

import com.runicgateway.app.data.api.dto.SafeUserDto
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Session lifecycle over a fake [TokenStore] (PLAN.md §4.3): restore on launch,
 * establish/rotate/tear-down, and the invariant that the in-memory token pair and
 * the observable [Session] state always agree.
 */
class SessionManagerTest {

    /** In-memory stand-in for EncryptedSharedPreferences. */
    private class FakeTokenStore(var stored: StoredSession? = null) : TokenStore {
        override fun load(): StoredSession? = stored
        override fun save(session: StoredSession) { stored = session }
        override fun clear() { stored = null }
    }

    private fun user(id: Long = 1, name: String = "alice", role: String = "player") =
        SafeUserDto(id = id, username = name, role = role)

    @Test fun startsSignedOutWithNoStoredSession() {
        val mgr = SessionManager(FakeTokenStore())
        assertEquals(Session.SignedOut, mgr.state.value)
        assertNull(mgr.currentAccessToken())
        assertNull(mgr.currentRefreshToken())
    }

    @Test fun restoresSignedInFromStore() {
        val store = FakeTokenStore(
            StoredSession("access-1", "refresh-1", 5, "bob", "admin"),
        )
        val mgr = SessionManager(store)
        val state = mgr.state.value
        assertTrue(state is Session.SignedIn)
        assertEquals("bob", (state as Session.SignedIn).user.username)
        assertEquals(Role.ADMIN, state.user.role)
        assertEquals("access-1", mgr.currentAccessToken())
        assertEquals("refresh-1", mgr.currentRefreshToken())
    }

    @Test fun signInPersistsAndExposesTokens() {
        val store = FakeTokenStore()
        val mgr = SessionManager(store)
        mgr.onSignedIn("access-A", "refresh-A", user(role = "player"))

        assertTrue(mgr.state.value is Session.SignedIn)
        assertEquals("access-A", mgr.currentAccessToken())
        assertEquals("refresh-A", mgr.currentRefreshToken())
        // Persisted so a relaunch restores it.
        assertEquals("refresh-A", store.stored?.refreshToken)
    }

    @Test fun refreshRotatesTokensWhileSignedIn() {
        val mgr = SessionManager(FakeTokenStore())
        mgr.onSignedIn("access-A", "refresh-A", user())
        mgr.onRefreshed("access-B", "refresh-B", user())

        assertEquals("access-B", mgr.currentAccessToken())
        assertEquals("refresh-B", mgr.currentRefreshToken())
    }

    @Test fun refreshIsNoOpAfterSignOut() {
        val mgr = SessionManager(FakeTokenStore())
        mgr.onSignedIn("access-A", "refresh-A", user())
        mgr.onSignedOut()
        // A refresh that races a logout must not resurrect the session.
        mgr.onRefreshed("access-B", "refresh-B", user())

        assertEquals(Session.SignedOut, mgr.state.value)
        assertNull(mgr.currentAccessToken())
    }

    @Test fun userRefreshUpdatesRoleKeepingTokens() {
        val mgr = SessionManager(FakeTokenStore())
        mgr.onSignedIn("access-A", "refresh-A", user(role = "player"))
        // A /auth/me re-validation reports a promotion.
        mgr.onUserRefreshed(user(role = "editor"))

        val state = mgr.state.value as Session.SignedIn
        assertEquals(Role.EDITOR, state.user.role)
        assertEquals("access-A", mgr.currentAccessToken())
    }

    @Test fun signOutClearsStoreAndState() {
        val store = FakeTokenStore()
        val mgr = SessionManager(store)
        mgr.onSignedIn("access-A", "refresh-A", user())
        mgr.onSignedOut()

        assertEquals(Session.SignedOut, mgr.state.value)
        assertNull(store.stored)
    }
}
