import { describe, it, expect, vi, beforeEach } from "vitest";
import { sendDiscordWebhook, sendStandingsUpdateNotification } from "../discord";

const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc";

function mockFetch(status: number, body = "") {
  return vi.fn().mockResolvedValue({
    ok: status >= 200 && status < 300,
    status,
    text: async () => body,
  });
}

function getDescription(): string {
  const body = JSON.parse(
    (fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body
  );
  return body.embeds[0].description as string;
}

describe("sendDiscordWebhook", () => {
  beforeEach(() => {
    vi.stubGlobal("fetch", mockFetch(204));
  });

  it("POSTs JSON to the webhook URL", async () => {
    await sendDiscordWebhook(WEBHOOK_URL, { content: "hello" });

    expect(fetch).toHaveBeenCalledWith(WEBHOOK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ content: "hello" }),
    });
  });

  it("throws on non-2xx response", async () => {
    vi.stubGlobal("fetch", mockFetch(400, "Bad Request"));

    await expect(sendDiscordWebhook(WEBHOOK_URL, {})).rejects.toThrow(
      "Discord webhook failed: 400"
    );
  });
});

describe("sendStandingsUpdateNotification", () => {
  beforeEach(() => {
    vi.stubGlobal("fetch", mockFetch(204));
  });

  it("sends an embed with the season name as title", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }],
      previousStandings: new Map([["a", 125]]),
    });

    const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
    expect(body.embeds[0].title).toBe("📊 Standings Update — My League 2025");
  });

  it("shows point deltas as integers in standings changes", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "Test League",
      standings: [
        { teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
        { teamId: "b", teamName: "Beta", totalPoints: 100, rank: 2 },
      ],
      previousStandings: new Map([
        ["a", 125],
        ["b", 100],
      ]),
    });

    const desc = getDescription();
    expect(desc).toContain("Alpha");
    expect(desc).toContain("+25 pts");
    expect(desc).not.toContain("+25.0");
    // Beta didn't change — not shown in standings changes
    expect(desc).not.toContain("Beta");
  });

  it("includes sport name and event name in header when both provided", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
      previousStandings: new Map([["a", 100]]),
      sportName: "UEFA Champions League",
      eventName: "Knockout Stage",
    });

    const desc = getDescription();
    expect(desc).toContain("UEFA Champions League — Knockout Stage");
  });

  it("shows only event name when no sport name provided", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
      previousStandings: new Map([["a", 100]]),
      eventName: "Quarter Finals",
    });

    const desc = getDescription();
    expect(desc).toContain("Quarter Finals");
    expect(desc).not.toContain("—\n");
  });

  it("omits header section when neither sport name nor event name provided", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
      previousStandings: new Map([["a", 100]]),
    });

    const desc = getDescription();
    expect(desc).toContain("**Standings Changes**");
    expect(desc).not.toContain("**Scored Matches**");
  });

  it("lists scored matches above standings changes", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
      previousStandings: new Map([["a", 100]]),
      scoredMatches: [
        { winnerName: "Real Madrid", loserName: "Bayern Munich", winnerUsername: "manager1" },
        { winnerName: "Arsenal", loserName: "PSG", loserUsername: "manager2" },
      ],
    });

    const desc = getDescription();
    expect(desc).toContain("**Scored Matches**");
    expect(desc).toContain("• **Real Madrid (manager1)** def. Bayern Munich");
    expect(desc).toContain("• **Arsenal** def. PSG (manager2)");
    // Scored matches section should appear before standings
    expect(desc.indexOf("Scored Matches")).toBeLessThan(desc.indexOf("Standings Changes"));
  });

  it("shows username in parentheses in standings changes when provided", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [
        { teamId: "a", teamName: "Alpha FC", username: "christhrowsrocks", totalPoints: 150, rank: 1 },
        { teamId: "b", teamName: "Beta United", totalPoints: 100, rank: 2 },
      ],
      previousStandings: new Map([
        ["a", 125],
        ["b", 100],
      ]),
    });

    const desc = getDescription();
    expect(desc).toContain("1. Alpha FC (christhrowsrocks) — 150 pts");
    // Beta didn't change points or rank, so it's not shown
    expect(desc).not.toContain("Beta United");
  });

  it("shows usernames in parentheses in match results when both are drafted", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
      previousStandings: new Map([["a", 100]]),
      scoredMatches: [
        {
          winnerName: "Sporting",
          loserName: "Bodø/Glimt",
          winnerUsername: "christhrowsrocks",
          loserUsername: "apatel",
        },
      ],
    });

    const desc = getDescription();
    expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt (apatel)");
  });

  it("omits scored matches where neither side has a username", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
      previousStandings: new Map([["a", 100]]),
      scoredMatches: [{ winnerName: "Real Madrid", loserName: "Bayern Munich" }],
    });

    const desc = getDescription();
    expect(desc).not.toContain("**Scored Matches**");
    expect(desc).not.toContain("Real Madrid");
    expect(desc).not.toContain("Bayern Munich");
  });

  it("omits username parenthesis only for the undrafted side in match results", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
      previousStandings: new Map([["a", 100]]),
      scoredMatches: [
        {
          winnerName: "Sporting",
          loserName: "Bodø/Glimt",
          winnerUsername: "christhrowsrocks",
        },
      ],
    });

    const desc = getDescription();
    expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt");
    expect(desc).not.toContain("Bodø/Glimt (");
  });

  it("shows teams that changed rank even without a point change", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [
        { teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
        { teamId: "b", teamName: "Beta", totalPoints: 125, rank: 2 },
        { teamId: "c", teamName: "Gamma", totalPoints: 100, rank: 3 },
      ],
      previousStandings: new Map([
        ["a", 125],
        ["b", 125],
        ["c", 100],
      ]),
      previousRanks: new Map([
        ["a", 2],
        ["b", 1],
        ["c", 3],
      ]),
    });

    const desc = getDescription();
    // Alpha gained points and moved up
    expect(desc).toContain("Alpha");
    expect(desc).toContain("+25 pts");
    expect(desc).toContain("↑1");
    // Beta didn't gain points but was displaced — rank changed
    expect(desc).toContain("Beta");
    expect(desc).toContain("↓1");
    // Gamma unchanged — not shown
    expect(desc).not.toContain("Gamma");
  });

  it("omits rank delta when no previousRanks provided", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [
        { teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
      ],
      previousStandings: new Map([["a", 125]]),
    });

    const desc = getDescription();
    expect(desc).toContain("Alpha");
    expect(desc).not.toContain("↑");
    expect(desc).not.toContain("↓");
  });

  it("escapes Discord markdown characters in team names and usernames", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [
        { teamId: "a", teamName: "Team__Underline", username: "user__name", totalPoints: 150, rank: 1 },
      ],
      previousStandings: new Map([["a", 125]]),
      scoredMatches: [
        {
          winnerName: "Winner*Bold*",
          loserName: "Loser~Strike~",
          winnerUsername: "user_one",
          loserUsername: "user_two",
        },
      ],
    });

    const desc = getDescription();
    expect(desc).toContain("Team\\_\\_Underline");
    expect(desc).toContain("user\\_\\_name");
    expect(desc).toContain("Winner\\*Bold\\*");
    expect(desc).toContain("Loser\\~Strike\\~");
    expect(desc).toContain("user\\_one");
    expect(desc).toContain("user\\_two");
  });

  it("does not show fields on the embed", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
      previousStandings: new Map([["a", 100]]),
    });

    const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
    expect(body.embeds[0].fields).toBeUndefined();
  });

  it("omits standings changes section when no teams changed", async () => {
    await sendStandingsUpdateNotification({
      webhookUrl: WEBHOOK_URL,
      seasonName: "My League 2025",
      standings: [
        { teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 },
        { teamId: "b", teamName: "Beta", totalPoints: 75, rank: 2 },
      ],
      previousStandings: new Map([
        ["a", 100],
        ["b", 75],
      ]),
      previousRanks: new Map([
        ["a", 1],
        ["b", 2],
      ]),
    });

    const desc = getDescription();
    expect(desc).not.toContain("**Standings Changes**");
    expect(desc).not.toContain("Alpha");
    expect(desc).not.toContain("Beta");
  });
});
