"""Per-workspace allow-list (managed Slack relay, M3.5).

Slack user ids are workspace-scoped, so each connected workspace (`slack:team:*`)
carries its OWN allow-list; a relay event is authorized against ITS team's list alone.
Team-less sources (manual Socket Mode) keep the flat `slack:default` list — these tests
guard both paths so neither regresses the other.
"""

from __future__ import annotations

from fastapi.testclient import TestClient

from coworker.connectors import (
    ConnectorSettings,
    Gateway,
    MessageEvent,
    SessionSource,
    TeamAuth,
    load_settings,
)
from coworker.connectors.config import is_authorized
from coworker.providers import ModelCapabilities, ProviderClient
from coworker.secrets import SecretStore
from coworker.server import create_app
from coworker.server.manager import SessionManager


class ScriptedProvider(ProviderClient):
    def complete(self, *, model, messages, tools=None, **settings):
        raise AssertionError("no turns expected")

    def capabilities(self, model):
        return ModelCapabilities()


def _relay_manager(tmp_path, *, teams=("T1",)) -> SessionManager:
    m = SessionManager(data_dir=tmp_path / "data", provider=ScriptedProvider())
    for t in teams:
        m.secrets.put(
            f"slack:team:{t}",
            {"type": "oauth", "managed": True, "bot_token": f"xoxb-{t}", "team_id": t},
        )
    m.secrets.put(
        "slack:default",
        {"type": "oauth", "managed": True, "mode": "relay", "enabled": True},
    )
    return m


# -- is_authorized ---------------------------------------------------------------
def test_is_authorized_team_scoped():
    s = ConnectorSettings(
        platform="slack",
        allowed_users={"U_FLAT"},
        teams={"T1": TeamAuth(allowed_users={"U_OK"}), "T2": TeamAuth()},
    )
    # authorized only via the event's OWN team's list
    assert is_authorized(s, SessionSource("slack", "C1", user_id="U_OK", team_id="T1"))
    assert not is_authorized(
        s, SessionSource("slack", "C1", user_id="U_OK", team_id="T2")
    )
    # the flat list never authorizes a team-scoped event
    assert not is_authorized(
        s, SessionSource("slack", "C1", user_id="U_FLAT", team_id="T1")
    )
    # unknown team = no install we know of → deny
    assert not is_authorized(
        s, SessionSource("slack", "C1", user_id="U_OK", team_id="T_UNKNOWN")
    )
    # per-team allow_all opens only that team
    s.teams["T2"].allow_all = True
    assert is_authorized(s, SessionSource("slack", "C1", user_id="U_X", team_id="T2"))
    assert not is_authorized(
        s, SessionSource("slack", "C1", user_id="U_X", team_id="T1")
    )


def test_is_authorized_flat_path_unchanged():
    # Manual Socket Mode sources carry no team_id → the flat list, exactly as before,
    # even when team lists exist alongside.
    s = ConnectorSettings(
        platform="slack",
        allowed_users={"U_FLAT"},
        teams={"T1": TeamAuth(allowed_users={"U_OK"}, allow_all=True)},
    )
    assert is_authorized(s, SessionSource("slack", "C1", user_id="U_FLAT"))
    assert not is_authorized(s, SessionSource("slack", "C1", user_id="U_OK"))
    s2 = ConnectorSettings(platform="slack", allow_all=True)
    assert is_authorized(s2, SessionSource("slack", "C1", user_id="anyone"))


# -- load_settings ---------------------------------------------------------------
def test_load_settings_populates_teams(tmp_path):
    secrets = SecretStore(tmp_path / "secrets.json")
    secrets.put("slack:default", {"type": "oauth", "mode": "relay", "enabled": True})
    secrets.put(
        "slack:team:T1",
        {"bot_token": "xoxb-1", "allowed_users": ["U_A", "U_B"]},
    )
    secrets.put("slack:team:T2", {"bot_token": "xoxb-2", "allow_all": True})
    settings = load_settings(secrets)
    slack = settings["slack"]
    assert slack.enabled is True
    assert slack.teams["T1"].allowed_users == {"U_A", "U_B"}
    assert slack.teams["T1"].allow_all is False
    assert slack.teams["T2"].allow_all is True


# -- manager write path ----------------------------------------------------------
def test_set_allowed_with_team_writes_team_profile(tmp_path):
    m = _relay_manager(tmp_path, teams=("T1", "T2"))
    m.gateway = Gateway(
        secrets=m.secrets, settings={"slack": load_settings(m.secrets)["slack"]}
    )

    out = m.allow_user("slack", "U_NEW", team_id="T1")
    assert out["ok"] is True and out["team_id"] == "T1"
    assert m.secrets.get("slack:team:T1")["allowed_users"] == ["U_NEW"]
    # the sibling team and the flat list are untouched
    assert not m.secrets.get("slack:team:T2").get("allowed_users")
    assert not m.secrets.get("slack:default").get("allowed_users")
    # live gateway reflects it without a restart
    assert m.gateway.settings["slack"].teams["T1"].allowed_users == {"U_NEW"}

    m.disallow_user("slack", "U_NEW", team_id="T1")
    assert m.secrets.get("slack:team:T1")["allowed_users"] == []
    assert m.gateway.settings["slack"].teams["T1"].allowed_users == set()

    # unknown workspace → error, nothing written
    assert m.allow_user("slack", "U_X", team_id="T_NOPE")["ok"] is False


def test_set_allowed_without_team_keeps_flat_behavior(tmp_path):
    m = _relay_manager(tmp_path)
    assert m.allow_user("slack", "U_FLAT")["allowed_users"] == ["U_FLAT"]
    assert m.secrets.get("slack:default")["allowed_users"] == ["U_FLAT"]
    assert not m.secrets.get("slack:team:T1").get("allowed_users")


# -- park + resolve --------------------------------------------------------------
async def test_park_carries_team_and_resolve_allows_into_team(tmp_path):
    m = _relay_manager(tmp_path, teams=("T1",))
    delivered: list[MessageEvent] = []

    async def _capture(event: MessageEvent) -> None:
        delivered.append(event)

    m._dispatch_inbound = _capture

    event = MessageEvent(
        text="hello from T1",
        source=SessionSource(
            "slack", "C9", user_id="U_STRANGER", user_name="Zed", team_id="T1"
        ),
    )
    await m._park_unauthorized(event)
    items = m.parked.list("slack")
    assert items[0]["team_id"] == "T1"

    out = await m.resolve_unauthorized("slack", items[0]["id"], "allow_deliver")
    assert out["ok"] is True
    # the allow landed on the WORKSPACE list, not the flat one
    assert m.secrets.get("slack:team:T1")["allowed_users"] == ["U_STRANGER"]
    assert not m.secrets.get("slack:default").get("allowed_users")
    # the replayed event keeps its workspace, so per-team auth re-checks correctly
    assert len(delivered) == 1
    assert delivered[0].source.team_id == "T1"
    assert delivered[0].text == "hello from T1"


async def test_resolve_teamless_parked_uses_flat_list(tmp_path):
    # Manual-mode parked items (no team_id) keep resolving into slack:default.
    m = _relay_manager(tmp_path)

    async def _noop(event) -> None:
        pass

    m._dispatch_inbound = _noop
    await m._park_unauthorized(
        MessageEvent(
            text="hi",
            source=SessionSource("slack", "D1", user_id="U_M", chat_type="dm"),
        )
    )
    item = m.parked.list("slack")[0]
    assert item["team_id"] is None
    assert (await m.resolve_unauthorized("slack", item["id"], "allow"))["ok"] is True
    assert m.secrets.get("slack:default")["allowed_users"] == ["U_M"]
    assert not m.secrets.get("slack:team:T1").get("allowed_users")


# -- REST + connector_list surface ------------------------------------------------
def test_rest_allow_with_team_and_workspaces_field(tmp_path):
    m = _relay_manager(tmp_path, teams=("T1", "T2"))
    client = TestClient(create_app(m))

    r = client.post(
        "/v1/connectors/slack/allow", json={"user_id": "U_W", "team_id": "T1"}
    )
    assert r.json()["ok"] is True
    slack = next(
        c
        for c in client.get("/v1/connectors").json()["connectors"]
        if c["name"] == "slack"
    )
    assert slack["connected"] is True and slack["mode"] == "relay"
    ws = {w["team_id"]: w for w in slack["workspaces"]}
    assert ws["T1"]["allowed_users"] == ["U_W"]
    assert ws["T2"]["allowed_users"] == []

    r = client.post(
        "/v1/connectors/slack/disallow", json={"user_id": "U_W", "team_id": "T1"}
    )
    assert r.json()["allowed_users"] == []


# -- installer pre-add on managed install (UX-027) --------------------------------
def test_managed_install_preadds_the_installer(tmp_path):
    from coworker.connectors.setup import managed_connect_slack_install

    s = SecretStore(tmp_path / "secrets.json")
    managed_connect_slack_install(
        s, {"team_id": "T1", "access_token": "xoxb-t1", "slack_user_id": "U_ME"}
    )
    assert s.get("slack:team:T1")["allowed_users"] == ["U_ME"]
    src = SessionSource("slack", "T1/C1", user_id="U_ME", team_id="T1")
    assert is_authorized(load_settings(s)["slack"], src) is True


def test_reinstall_preserves_the_existing_allow_list(tmp_path):
    from coworker.connectors.setup import managed_connect_slack_install

    s = SecretStore(tmp_path / "secrets.json")
    s.put(
        "slack:team:T1",
        {
            "bot_token": "xoxb-old",
            "allowed_users": ["U_ANNA", "U_ME"],
            "sender_name": "Rohit",
        },
    )
    managed_connect_slack_install(
        s, {"team_id": "T1", "access_token": "xoxb-new", "slack_user_id": "U_ME"}
    )
    profile = s.get("slack:team:T1")
    assert profile["allowed_users"] == ["U_ANNA", "U_ME"]
    assert profile["bot_token"] == "xoxb-new"
    assert profile["sender_name"] == "Rohit"


def test_workspace_listing_carries_installer_identity(tmp_path):
    from coworker.connectors.setup import (
        _slack_workspaces,
        managed_connect_slack_install,
    )

    s = SecretStore(tmp_path / "secrets.json")
    managed_connect_slack_install(
        s, {"team_id": "T1", "access_token": "xoxb-t1", "slack_user_id": "U_ME"}
    )
    (w,) = _slack_workspaces(s)
    assert w["installer_user_id"] == "U_ME"
    assert w["approval_owner_ids"] == ["U_ME"]
    assert w["installer_name"] == ""
