import { describe, expect, test } from "vitest";

import type { ClawdbotConfig } from "../config/config.js";
import { resolveAgentRoute } from "./resolve-route.js";

describe("resolveAgentRoute", () => {
  test("defaults to main/default when no bindings exist", () => {
    const cfg: ClawdbotConfig = {};
    const route = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: null,
      peer: { kind: "dm", id: "+15551234567" },
    });
    expect(route.agentId).toBe("main");
    expect(route.accountId).toBe("default");
    expect(route.sessionKey).toBe("agent:main:main");
    expect(route.matchedBy).toBe("default");
  });

  test("dmScope=per-peer isolates DM sessions by sender id", () => {
    const cfg: ClawdbotConfig = {
      session: { dmScope: "per-peer" },
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: null,
      peer: { kind: "dm", id: "+15551234567" },
    });
    expect(route.sessionKey).toBe("agent:main:dm:+15551234567");
  });

  test("dmScope=per-channel-peer isolates DM sessions per channel and sender", () => {
    const cfg: ClawdbotConfig = {
      session: { dmScope: "per-channel-peer" },
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: null,
      peer: { kind: "dm", id: "+15551234567" },
    });
    expect(route.sessionKey).toBe("agent:main:whatsapp:dm:+15551234567");
  });

  test("identityLinks collapses per-peer DM sessions across providers", () => {
    const cfg: ClawdbotConfig = {
      session: {
        dmScope: "per-peer",
        identityLinks: {
          alice: ["telegram:111111111", "discord:222222222222222222"],
        },
      },
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "telegram",
      accountId: null,
      peer: { kind: "dm", id: "111111111" },
    });
    expect(route.sessionKey).toBe("agent:main:dm:alice");
  });

  test("identityLinks applies to per-channel-peer DM sessions", () => {
    const cfg: ClawdbotConfig = {
      session: {
        dmScope: "per-channel-peer",
        identityLinks: {
          alice: ["telegram:111111111", "discord:222222222222222222"],
        },
      },
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      accountId: null,
      peer: { kind: "dm", id: "222222222222222222" },
    });
    expect(route.sessionKey).toBe("agent:main:discord:dm:alice");
  });

  test("peer binding wins over account binding", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "a",
          match: {
            channel: "whatsapp",
            accountId: "biz",
            peer: { kind: "dm", id: "+1000" },
          },
        },
        {
          agentId: "b",
          match: { channel: "whatsapp", accountId: "biz" },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: "biz",
      peer: { kind: "dm", id: "+1000" },
    });
    expect(route.agentId).toBe("a");
    expect(route.sessionKey).toBe("agent:a:main");
    expect(route.matchedBy).toBe("binding.peer");
  });

  test("discord channel peer binding wins over guild binding", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "chan",
          match: {
            channel: "discord",
            accountId: "default",
            peer: { kind: "channel", id: "c1" },
          },
        },
        {
          agentId: "guild",
          match: {
            channel: "discord",
            accountId: "default",
            guildId: "g1",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      accountId: "default",
      peer: { kind: "channel", id: "c1" },
      guildId: "g1",
    });
    expect(route.agentId).toBe("chan");
    expect(route.sessionKey).toBe("agent:chan:discord:channel:c1");
    expect(route.matchedBy).toBe("binding.peer");
  });

  test("guild binding wins over account binding when peer not bound", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "guild",
          match: {
            channel: "discord",
            accountId: "default",
            guildId: "g1",
          },
        },
        {
          agentId: "acct",
          match: { channel: "discord", accountId: "default" },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      accountId: "default",
      peer: { kind: "channel", id: "c1" },
      guildId: "g1",
    });
    expect(route.agentId).toBe("guild");
    expect(route.matchedBy).toBe("binding.guild");
  });

  test("peer+guild binding does not act as guild-wide fallback when peer mismatches (#14752)", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "olga",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "CHANNEL_A" },
            guildId: "GUILD_1",
          },
        },
        {
          agentId: "main",
          match: {
            channel: "discord",
            guildId: "GUILD_1",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "CHANNEL_B" },
      guildId: "GUILD_1",
    });
    expect(route.agentId).toBe("main");
    expect(route.matchedBy).toBe("binding.guild");
  });

  test("peer+guild binding requires guild match even when peer matches", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "wrongguild",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "c1" },
            guildId: "g1",
          },
        },
        {
          agentId: "rightguild",
          match: {
            channel: "discord",
            guildId: "g2",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "c1" },
      guildId: "g2",
    });
    expect(route.agentId).toBe("rightguild");
    expect(route.matchedBy).toBe("binding.guild");
  });

  test("peer+team binding does not act as team-wide fallback when peer mismatches", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "roomonly",
          match: {
            channel: "slack",
            peer: { kind: "channel", id: "C_A" },
            teamId: "T1",
          },
        },
        {
          agentId: "teamwide",
          match: {
            channel: "slack",
            teamId: "T1",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "slack",
      teamId: "T1",
      peer: { kind: "channel", id: "C_B" },
    });
    expect(route.agentId).toBe("teamwide");
    expect(route.matchedBy).toBe("binding.team");
  });

  test("peer+team binding requires team match even when peer matches", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "wrongteam",
          match: {
            channel: "slack",
            peer: { kind: "channel", id: "C1" },
            teamId: "T1",
          },
        },
        {
          agentId: "rightteam",
          match: {
            channel: "slack",
            teamId: "T2",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "slack",
      teamId: "T2",
      peer: { kind: "channel", id: "C1" },
    });
    expect(route.agentId).toBe("rightteam");
    expect(route.matchedBy).toBe("binding.team");
  });

  test("missing accountId in binding matches default account only", () => {
    const cfg: ClawdbotConfig = {
      bindings: [{ agentId: "defaultAcct", match: { channel: "whatsapp" } }],
    };

    const defaultRoute = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: undefined,
      peer: { kind: "dm", id: "+1000" },
    });
    expect(defaultRoute.agentId).toBe("defaultacct");
    expect(defaultRoute.matchedBy).toBe("binding.account");

    const otherRoute = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: "biz",
      peer: { kind: "dm", id: "+1000" },
    });
    expect(otherRoute.agentId).toBe("main");
  });

  test("accountId=* matches any account as a channel fallback", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "any",
          match: { channel: "whatsapp", accountId: "*" },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: "biz",
      peer: { kind: "dm", id: "+1000" },
    });
    expect(route.agentId).toBe("any");
    expect(route.matchedBy).toBe("binding.channel");
  });

  test("defaultAgentId is used when no binding matches", () => {
    const cfg: ClawdbotConfig = {
      agents: {
        list: [{ id: "home", default: true, workspace: "~/clawd-home" }],
      },
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: "biz",
      peer: { kind: "dm", id: "+1000" },
    });
    expect(route.agentId).toBe("home");
    expect(route.sessionKey).toBe("agent:home:main");
  });
});

test("dmScope=per-account-channel-peer isolates DM sessions per account, channel and sender", () => {
  const cfg: ClawdbotConfig = {
    session: { dmScope: "per-account-channel-peer" },
  };
  const route = resolveAgentRoute({
    cfg,
    channel: "telegram",
    accountId: "tasks",
    peer: { kind: "direct", id: "7550356539" },
  });
  expect(route.sessionKey).toBe("agent:main:telegram:tasks:direct:7550356539");
});

test("dmScope=per-account-channel-peer uses default accountId when not provided", () => {
  const cfg: ClawdbotConfig = {
    session: { dmScope: "per-account-channel-peer" },
  };
  const route = resolveAgentRoute({
    cfg,
    channel: "telegram",
    accountId: null,
    peer: { kind: "direct", id: "7550356539" },
  });
  expect(route.sessionKey).toBe("agent:main:telegram:default:direct:7550356539");
});

describe("parentPeer binding inheritance (thread support)", () => {
  test("thread inherits binding from parent channel when no direct match", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "adecco",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "parent-channel-123" },
    });
    expect(route.agentId).toBe("adecco");
    expect(route.matchedBy).toBe("binding.peer.parent");
  });

  test("direct peer binding wins over parent peer binding", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "thread-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "thread-456" },
          },
        },
        {
          agentId: "parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "parent-channel-123" },
    });
    expect(route.agentId).toBe("thread-agent");
    expect(route.matchedBy).toBe("binding.peer");
  });

  test("parent peer binding wins over guild binding", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
        {
          agentId: "guild-agent",
          match: {
            channel: "discord",
            guildId: "guild-789",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "parent-channel-123" },
      guildId: "guild-789",
    });
    expect(route.agentId).toBe("parent-agent");
    expect(route.matchedBy).toBe("binding.peer.parent");
  });

  test("falls back to guild binding when no parent peer match", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "other-parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "other-parent-999" },
          },
        },
        {
          agentId: "guild-agent",
          match: {
            channel: "discord",
            guildId: "guild-789",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "parent-channel-123" },
      guildId: "guild-789",
    });
    expect(route.agentId).toBe("guild-agent");
    expect(route.matchedBy).toBe("binding.guild");
  });

  test("parentPeer with empty id is ignored", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "" },
    });
    expect(route.agentId).toBe("main");
    expect(route.matchedBy).toBe("default");
  });

  test("null parentPeer is handled gracefully", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: null,
    });
    expect(route.agentId).toBe("main");
    expect(route.matchedBy).toBe("default");
  });
});

describe("backward compatibility: peer.kind dm → direct", () => {
  test("legacy dm in config matches runtime direct peer", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "alex",
          match: {
            channel: "whatsapp",
            // Legacy config uses "dm" instead of "direct"
            peer: { kind: "dm" as ChatType, id: "+15551234567" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "whatsapp",
      accountId: null,
      // Runtime uses canonical "direct"
      peer: { kind: "direct", id: "+15551234567" },
    });
    expect(route.agentId).toBe("alex");
    expect(route.matchedBy).toBe("binding.peer");
  });
});

describe("role-based agent routing", () => {
  test("guild+roles binding matches when member has matching role", () => {
    const cfg: ClawdbotConfig = {
      bindings: [{ agentId: "opus", match: { channel: "discord", guildId: "g1", roles: ["r1"] } }],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["r1"],
      peer: { kind: "channel", id: "c1" },
    });
    expect(route.agentId).toBe("opus");
    expect(route.matchedBy).toBe("binding.guild+roles");
  });

  test("guild+roles binding skipped when no matching role", () => {
    const cfg: ClawdbotConfig = {
      bindings: [{ agentId: "opus", match: { channel: "discord", guildId: "g1", roles: ["r1"] } }],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["r2"],
      peer: { kind: "channel", id: "c1" },
    });
    expect(route.agentId).toBe("main");
    expect(route.matchedBy).toBe("default");
  });

  test("guild+roles is more specific than guild-only", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        { agentId: "opus", match: { channel: "discord", guildId: "g1", roles: ["r1"] } },
        { agentId: "sonnet", match: { channel: "discord", guildId: "g1" } },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["r1"],
      peer: { kind: "channel", id: "c1" },
    });
    expect(route.agentId).toBe("opus");
    expect(route.matchedBy).toBe("binding.guild+roles");
  });

  test("peer binding still beats guild+roles", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "peer-agent",
          match: { channel: "discord", peer: { kind: "channel", id: "c1" } },
        },
        { agentId: "roles-agent", match: { channel: "discord", guildId: "g1", roles: ["r1"] } },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["r1"],
      peer: { kind: "channel", id: "c1" },
    });
    expect(route.agentId).toBe("peer-agent");
    expect(route.matchedBy).toBe("binding.peer");
  });

  test("parent peer binding still beats guild+roles", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "parent-agent",
          match: { channel: "discord", peer: { kind: "channel", id: "parent-1" } },
        },
        { agentId: "roles-agent", match: { channel: "discord", guildId: "g1", roles: ["r1"] } },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["r1"],
      peer: { kind: "channel", id: "thread-1" },
      parentPeer: { kind: "channel", id: "parent-1" },
    });
    expect(route.agentId).toBe("parent-agent");
    expect(route.matchedBy).toBe("binding.peer.parent");
  });

  test("no memberRoleIds means guild+roles doesn't match", () => {
    const cfg: ClawdbotConfig = {
      bindings: [{ agentId: "opus", match: { channel: "discord", guildId: "g1", roles: ["r1"] } }],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      peer: { kind: "channel", id: "c1" },
    });
    expect(route.agentId).toBe("main");
    expect(route.matchedBy).toBe("default");
  });

  test("first matching binding wins with multiple role bindings", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        { agentId: "opus", match: { channel: "discord", guildId: "g1", roles: ["r1"] } },
        { agentId: "sonnet", match: { channel: "discord", guildId: "g1", roles: ["r2"] } },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["r1", "r2"],
      peer: { kind: "channel", id: "c1" },
    });
    expect(route.agentId).toBe("opus");
    expect(route.matchedBy).toBe("binding.guild+roles");
  });

  test("empty roles array treated as no role restriction", () => {
    const cfg: ClawdbotConfig = {
      bindings: [{ agentId: "opus", match: { channel: "discord", guildId: "g1", roles: [] } }],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["r1"],
      peer: { kind: "channel", id: "c1" },
    });
    expect(route.agentId).toBe("opus");
    expect(route.matchedBy).toBe("binding.guild");
  });

  test("guild+roles binding does not match as guild-only when roles do not match", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        { agentId: "opus", match: { channel: "discord", guildId: "g1", roles: ["admin"] } },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["regular"],
      peer: { kind: "channel", id: "c1" },
    });
    expect(route.agentId).toBe("main");
    expect(route.matchedBy).toBe("default");
  });

  test("peer+guild+roles binding does not act as guild+roles fallback when peer mismatches", () => {
    const cfg: ClawdbotConfig = {
      bindings: [
        {
          agentId: "peer-roles",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "c-target" },
            guildId: "g1",
            roles: ["r1"],
          },
        },
        {
          agentId: "guild-roles",
          match: {
            channel: "discord",
            guildId: "g1",
            roles: ["r1"],
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      guildId: "g1",
      memberRoleIds: ["r1"],
      peer: { kind: "channel", id: "c-other" },
    });
    expect(route.agentId).toBe("guild-roles");
    expect(route.matchedBy).toBe("binding.guild+roles");
  });
});

describe("parentPeer binding inheritance (thread support)", () => {
  test("thread inherits binding from parent channel when no direct match", () => {
    const cfg: MoltbotConfig = {
      bindings: [
        {
          agentId: "adecco",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "parent-channel-123" },
    });
    expect(route.agentId).toBe("adecco");
    expect(route.matchedBy).toBe("binding.peer.parent");
  });

  test("direct peer binding wins over parent peer binding", () => {
    const cfg: MoltbotConfig = {
      bindings: [
        {
          agentId: "thread-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "thread-456" },
          },
        },
        {
          agentId: "parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "parent-channel-123" },
    });
    expect(route.agentId).toBe("thread-agent");
    expect(route.matchedBy).toBe("binding.peer");
  });

  test("parent peer binding wins over guild binding", () => {
    const cfg: MoltbotConfig = {
      bindings: [
        {
          agentId: "parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
        {
          agentId: "guild-agent",
          match: {
            channel: "discord",
            guildId: "guild-789",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "parent-channel-123" },
      guildId: "guild-789",
    });
    expect(route.agentId).toBe("parent-agent");
    expect(route.matchedBy).toBe("binding.peer.parent");
  });

  test("falls back to guild binding when no parent peer match", () => {
    const cfg: MoltbotConfig = {
      bindings: [
        {
          agentId: "other-parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "other-parent-999" },
          },
        },
        {
          agentId: "guild-agent",
          match: {
            channel: "discord",
            guildId: "guild-789",
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "parent-channel-123" },
      guildId: "guild-789",
    });
    expect(route.agentId).toBe("guild-agent");
    expect(route.matchedBy).toBe("binding.guild");
  });

  test("parentPeer with empty id is ignored", () => {
    const cfg: MoltbotConfig = {
      bindings: [
        {
          agentId: "parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: { kind: "channel", id: "" },
    });
    expect(route.agentId).toBe("main");
    expect(route.matchedBy).toBe("default");
  });

  test("null parentPeer is handled gracefully", () => {
    const cfg: MoltbotConfig = {
      bindings: [
        {
          agentId: "parent-agent",
          match: {
            channel: "discord",
            peer: { kind: "channel", id: "parent-channel-123" },
          },
        },
      ],
    };
    const route = resolveAgentRoute({
      cfg,
      channel: "discord",
      peer: { kind: "channel", id: "thread-456" },
      parentPeer: null,
    });
    expect(route.agentId).toBe("main");
    expect(route.matchedBy).toBe("default");
  });
});
