"""Behavior coverage for #4755 profile default workspace precedence."""

from __future__ import annotations

import json
import shutil
import subprocess
import textwrap
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]
SESSIONS_JS = ROOT / "static" / "sessions.js"
PANELS_JS = ROOT / "static" / "panels.js"
NODE = shutil.which("node")

node_test = pytest.mark.skipif(NODE is None, reason="node not on PATH")


def _extract_async_function(source: str, name: str) -> str:
    marker = f"async function {name}("
    start = source.find(marker)
    assert start >= 0, f"{name}() function must exist"
    brace = source.find("{", source.find(")", start))
    assert brace > start, f"{name}() function body must start"
    depth = 0
    in_string = None
    escaped = False
    in_line_comment = False
    in_block_comment = False
    for idx in range(brace, len(source)):
        ch = source[idx]
        nxt = source[idx + 1] if idx + 1 < len(source) else ""
        if in_line_comment:
            if ch == "\n":
                in_line_comment = False
            continue
        if in_block_comment:
            if ch == "*" and nxt == "/":
                in_block_comment = False
            continue
        if in_string:
            if escaped:
                escaped = False
            elif ch == "\\":
                escaped = True
            elif ch == in_string:
                in_string = None
            continue
        if ch == "/" and nxt == "/":
            in_line_comment = True
            continue
        if ch == "/" and nxt == "*":
            in_block_comment = True
            continue
        if ch in ("'", '"', "`"):
            in_string = ch
            continue
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                return source[start : idx + 1]
    raise AssertionError(f"could not extract {name}()")


def _run_node(script: str) -> dict:
    result = subprocess.run(
        [NODE, "-e", script],
        cwd=ROOT,
        text=True,
        capture_output=True,
        timeout=30,
        check=False,
    )
    assert result.returncode == 0, result.stderr or result.stdout
    return json.loads(result.stdout)


def _new_session_driver(session_workspace: str, default_workspace: str, switch_workspace: str | None) -> str:
    new_session = _extract_async_function(SESSIONS_JS.read_text(encoding="utf-8"), "newSession")
    return textwrap.dedent(
        f"""
        let captured=null;
        var _newSessionInFlight=null;
        var _messagesTruncated=false;
        var _oldestIdx=0;
        var _activeProject=null;
        var NO_PROJECT_FILTER='__all__';
        var _sessionSourceFilter='webui';
        var S={{
          session:{{session_id:'previous-session',workspace:{json.dumps(session_workspace)}}},
          _profileDefaultWorkspace:{json.dumps(default_workspace)},
          _profileSwitchWorkspace:{json.dumps(switch_workspace)},
          activeProfile:'default',
          toolCalls:[],
        }};
        global.window={{}};
        global.document={{createElement:()=>({{dataset:{{}},appendChild:()=>{{}}}})}};
        global.localStorage={{setItem:()=>{{}}}};
        function $(id){{return null;}}
        function _newSessionPendingText(){{return 'Creating';}}
        function _setNewSessionPending(){{}}
        function updateQueueBadge(){{}}
        function clearLiveToolCards(){{}}
        function api(path,opts){{
          captured={{path,body:JSON.parse(opts.body)}};
          return Promise.resolve({{session:{{session_id:'new-session',messages:[],workspace:captured.body.workspace}}}});
        }}
        function _rememberNewChatDraftSession(){{}}
        function _setActiveSessionUrl(){{}}
        function _setSessionViewedCount(){{}}
        function updateSendBtn(){{}}
        function setStatus(){{}}
        function setComposerStatus(){{}}
        function syncTopbar(){{}}
        function renderMessages(){{}}
        function loadDir(){{return Promise.resolve();}}
        {new_session}
        newSession().then(()=>{{
          process.stdout.write(JSON.stringify({{
            captured,
            switchWorkspace:S._profileSwitchWorkspace,
            profileDefaultWorkspace:S._profileDefaultWorkspace,
          }}));
        }}).catch(err=>{{
          console.error(err && err.stack || err);
          process.exit(1);
        }});
        """
    )


@node_test
def test_new_session_prefers_current_session_workspace_over_profile_default():
    """Deliberate divergence from the precedence introduced in #4755.

    #4755 made the "profile default" win over the current conversation's workspace.
    That precedence assumes the profile default is a stable, configured value. It is
    not, for a structural reason:

    ``api/workspace.py::get_profile_default_workspace()`` reads ``last_workspace.txt``
    BEFORE the configured value, and for the ``default`` profile that file is the
    GLOBAL one. ``set_last_workspace()`` rewrites it on every ``/api/chat/start`` and
    ``/api/session/update`` from ANY conversation.

    So ``S._profileDefaultWorkspace`` — hydrated from ``/api/profile/active`` — is not a
    configured default: it is a volatile shared pointer. "profile default wins over the
    current conversation" therefore degrades into "the workspace last used by ANOTHER
    conversation wins over the conversation I am actually in". With several
    conversations open on different workspaces, opening a new chat from conversation A
    created it on conversation B's workspace.

    Precedence used here: one-shot profile switch -> current conversation -> profile
    default. The profile default remains the blank-page fallback (#804/#5169) and is
    never consumed (#823).

    MAINTAINER NOTE: this intentionally diverges from #4755 and will conflict if that
    precedence is restored. Please do not "fix" it by reinstating the previous order
    without first addressing the volatility of ``get_profile_default_workspace()``.
    """
    payload = _run_node(_new_session_driver(
        session_workspace="/current-workspace",
        default_workspace="/profile-default",
        switch_workspace=None,
    ))

    assert payload["captured"]["path"] == "/api/session/new"
    assert payload["captured"]["body"]["workspace"] == "/current-workspace"
    assert payload["captured"]["body"]["prev_session_id"] == "previous-session"
    assert payload["captured"]["body"]["workspace_inherited_from_prev_session"] is True
    assert payload["profileDefaultWorkspace"] == "/profile-default"


@node_test
def test_new_session_blank_page_falls_back_to_profile_default():
    """Sans conversation chargée, le défaut de profil reste le repli (#804/#5169)."""
    driver = _new_session_driver(
        session_workspace="/current-workspace",
        default_workspace="/profile-default",
        switch_workspace=None,
    ).replace(
        "session:{session_id:'previous-session',workspace:\"/current-workspace\"},",
        "session:null,",
    )
    payload = _run_node(driver)

    assert payload["captured"]["body"]["workspace"] == "/profile-default"
    assert "workspace_inherited_from_prev_session" not in payload["captured"]["body"]
    assert payload["profileDefaultWorkspace"] == "/profile-default"



@node_test
def test_new_session_one_shot_switch_workspace_still_wins_and_clears():
    payload = _run_node(_new_session_driver(
        session_workspace="/current-workspace",
        default_workspace="/profile-default",
        switch_workspace="/explicit-switch",
    ))

    assert payload["captured"]["body"]["workspace"] == "/explicit-switch"
    assert "workspace_inherited_from_prev_session" not in payload["captured"]["body"]
    assert payload["switchWorkspace"] is None
    assert payload["profileDefaultWorkspace"] == "/profile-default"


@node_test
def test_parallel_conversations_each_inherit_their_own_workspace():
    first = _run_node(_new_session_driver(
        session_workspace="/workspace-a",
        default_workspace="/shared-profile-pointer",
        switch_workspace=None,
    ))
    second = _run_node(_new_session_driver(
        session_workspace="/workspace-b",
        default_workspace="/shared-profile-pointer",
        switch_workspace=None,
    ))

    assert first["captured"]["body"]["workspace"] == "/workspace-a"
    assert second["captured"]["body"]["workspace"] == "/workspace-b"
    assert first["captured"]["body"]["workspace_inherited_from_prev_session"] is True
    assert second["captured"]["body"]["workspace_inherited_from_prev_session"] is True


@node_test
def test_busy_workspace_switch_returns_before_session_update():
    switch_to_workspace = _extract_async_function(PANELS_JS.read_text(encoding="utf-8"), "switchToWorkspace")
    script = textwrap.dedent(
        f"""
        const calls=[];
        var S={{busy:true,session:{{session_id:'session-1',workspace:'/old',model:'gpt-5',model_provider:null}}}};
        function t(key){{return key;}}
        function showToast(message){{calls.push(['toast',message]);}}
        function api(path,opts){{calls.push(['api',path]);return Promise.resolve({{}});}}
        {switch_to_workspace}
        switchToWorkspace('/new','New').then(()=>{{
          process.stdout.write(JSON.stringify({{calls}}));
        }}).catch(err=>{{
          console.error(err && err.stack || err);
          process.exit(1);
        }});
        """
    )
    payload = _run_node(script)

    assert payload["calls"] == [["toast", "workspace_busy_switch"]]
