import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
import type { DraftIneligibilityReason } from "~/lib/draft-eligibility";

vi.mock("@tanstack/react-virtual", () => ({
  useVirtualizer: ({
    count,
    estimateSize,
  }: {
    count: number;
    estimateSize: (index: number) => number;
  }) => {
    const size = estimateSize(0);
    const items = Array.from({ length: count }, (_, i) => ({
      index: i,
      start: i * size,
      size,
      key: i,
    }));
    return {
      getTotalSize: () => count * size,
      getVirtualItems: () => items,
      measureElement: vi.fn(),
    };
  },
}));

const defaultProps = {
  participants: [
    { id: "1", name: "Player A", sport: { id: "s1", name: "NFL" } },
    { id: "2", name: "Player B", sport: { id: "s2", name: "NBA" } },
    { id: "3", name: "Player C", sport: { id: "s1", name: "NFL" } },
  ],
  searchQuery: "",
  sportFilters: [] as string[],
  hideDrafted: false,
  hideIneligible: false,
  hideCompletedSports: false,
  uniqueSports: ["NBA", "NFL"],
  userDraftedSportNames: new Set<string>(),
  draftedParticipantIds: new Set<string>(),
  queue: [] as Array<{ id: string; participantId: string }>,
  eligibility: null,
  canPick: false,
  hasTeam: false,
  onSearchChange: vi.fn(),
  onSportFiltersChange: vi.fn(),
  onHideDraftedChange: vi.fn(),
  onHideIneligibleChange: vi.fn(),
  onHideCompletedSportsChange: vi.fn(),
  onMakePick: vi.fn(),
  onAddToQueue: vi.fn(),
  onRemoveFromQueue: vi.fn(),
  watchedParticipantIds: new Set<string>(),
  onToggleWatchlist: vi.fn(),
  showOnlyWatched: false,
  onShowOnlyWatchedChange: vi.fn(),
  pickAnimationSignal: null,
  participantRanks: new Map([
    ["1", { overallRank: 1, sportRank: 1 }],
    ["2", { overallRank: 2, sportRank: 1 }],
    ["3", { overallRank: 3, sportRank: 2 }],
  ]),
};

// Both the mobile Sheet trigger and desktop Popover trigger are rendered in jsdom
// (CSS breakpoints are not applied). Click the first one to open the Sheet.
let user: ReturnType<typeof userEvent.setup>;

function createReason(
  code: DraftIneligibilityReason["code"],
  message: string
): DraftIneligibilityReason {
  return { code, message };
}

function clickFirstSportFilterTrigger(name: string | RegExp) {
  return user.click(screen.getAllByRole("button", { name })[0]);
}

describe("AvailableParticipantsSection", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    user = userEvent.setup();
  });

  describe("Rendering", () => {
    it("renders all participants by name", () => {
      render(<AvailableParticipantsSection {...defaultProps} />);
      expect(screen.getAllByText("Player A").length).toBeGreaterThan(0);
      expect(screen.getAllByText("Player B").length).toBeGreaterThan(0);
      expect(screen.getAllByText("Player C").length).toBeGreaterThan(0);
    });

    it("renders search input", () => {
      render(<AvailableParticipantsSection {...defaultProps} />);
      expect(
        screen.getByPlaceholderText("Search participants...")
      ).toBeInTheDocument();
    });

    it("renders the Show Drafted toggle", () => {
      render(<AvailableParticipantsSection {...defaultProps} />);
      expect(screen.getByText("Show Drafted")).toBeInTheDocument();
    });

    it("renders Show Ineligible and Show drafted sports toggles when hasTeam=true", async () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          eligibility={{ eligibleSportIds: new Set(["s1"]), ineligibleReasons: {} }}
        />
      );
      expect(screen.getByText("Show Ineligible")).toBeInTheDocument();
      // Show drafted sports lives inside the sport filter dropdown
      await clickFirstSportFilterTrigger("Filter by sport: All Sports");
      const dialog = screen.getByRole("dialog");
      expect(within(dialog).getByText("Show drafted sports")).toBeInTheDocument();
    });

    it("does not render Show Ineligible when hasTeam=false", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={false}
          eligibility={{ eligibleSportIds: new Set(), ineligibleReasons: {} }}
        />
      );
      expect(screen.queryByText("Show Ineligible")).not.toBeInTheDocument();
    });

    it("renders empty state when no participants", () => {
      render(
        <AvailableParticipantsSection {...defaultProps} participants={[]} />
      );
      expect(screen.getAllByText("No participants found.").length).toBeGreaterThan(0);
    });
  });

  describe("Sport Filter - trigger label", () => {
    it('shows "All Sports" in aria-label when no filters selected', () => {
      render(<AvailableParticipantsSection {...defaultProps} />);
      // Both mobile and desktop triggers share the same label
      const triggers = screen.getAllByRole("button", {
        name: "Filter by sport: All Sports",
      });
      expect(triggers.length).toBeGreaterThanOrEqual(1);
    });

    it("shows sport name in aria-label when exactly one filter selected", () => {
      render(
        <AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
      );
      const triggers = screen.getAllByRole("button", {
        name: "Filter by sport: NFL",
      });
      expect(triggers.length).toBeGreaterThanOrEqual(1);
    });

    it("shows count in aria-label when multiple filters selected", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          sportFilters={["NFL", "NBA"]}
        />
      );
      const triggers = screen.getAllByRole("button", {
        name: "Filter by sport: 2 sports selected",
      });
      expect(triggers.length).toBeGreaterThanOrEqual(1);
    });

    it("does not render active-filter badges", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          sportFilters={["NFL", "NBA"]}
        />
      );
      expect(screen.queryByLabelText(/Remove .* filter/)).not.toBeInTheDocument();
    });
  });

  describe("Projected Pick Dividers", () => {
    const manyParticipants = Array.from({ length: 30 }, (_, i) => ({
      id: String(i + 1),
      name: `Player ${i + 1}`,
      sport: { id: "s1", name: "NFL" },
    }));

    const manyRanks = new Map(
      Array.from({ length: 30 }, (_, i) => [
        String(i + 1),
        { overallRank: i + 1, sportRank: i + 1 },
      ])
    );

    it("renders projected pick divider at correct position", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
          projectedPicks={[
            { round: 3, picksFromNow: 10 },
            { round: 4, picksFromNow: 20 },
          ]}
        />
      );

      expect(screen.getByText("Projected Round 3 Pick")).toBeInTheDocument();
      expect(screen.getByText("Projected Round 4 Pick")).toBeInTheDocument();
    });

    it("does not render dividers when projectedPicks is undefined", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
        />
      );

      expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
    });

    it("does not render dividers when projectedPicks is empty", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
          projectedPicks={[]}
        />
      );

      expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
    });

    it("skips dividers where picksFromNow exceeds list length", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
          projectedPicks={[
            { round: 3, picksFromNow: 10 },
            { round: 10, picksFromNow: 100 },
          ]}
        />
      );

      expect(screen.getByText("Projected Round 3 Pick")).toBeInTheDocument();
      expect(screen.queryByText("Projected Round 10 Pick")).not.toBeInTheDocument();
    });

    it("skips dividers where picksFromNow is zero or negative", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
          projectedPicks={[
            { round: 1, picksFromNow: 0 },
            { round: 2, picksFromNow: 5 },
          ]}
        />
      );

      expect(screen.queryByText("Projected Round 1 Pick")).not.toBeInTheDocument();
      expect(screen.getByText("Projected Round 2 Pick")).toBeInTheDocument();
    });

    it("still renders all participants alongside dividers", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
          projectedPicks={[{ round: 3, picksFromNow: 10 }]}
        />
      );

      expect(screen.getAllByText("Player 1").length).toBeGreaterThan(0);
      expect(screen.getAllByText("Player 10").length).toBeGreaterThan(0);
      expect(screen.getAllByText("Player 30").length).toBeGreaterThan(0);
    });

    it("suppresses dividers when search query is active", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
          searchQuery="Player"
          projectedPicks={[{ round: 3, picksFromNow: 10 }]}
        />
      );

      expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
    });

    it("suppresses dividers when sport filter is active", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
          sportFilters={["NFL"]}
          projectedPicks={[{ round: 3, picksFromNow: 10 }]}
        />
      );

      expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
    });

    it("suppresses dividers when hideDrafted is active", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          participants={manyParticipants}
          participantRanks={manyRanks}
          hideDrafted
          projectedPicks={[{ round: 3, picksFromNow: 10 }]}
        />
      );

      expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
    });
  });

  describe("Sport Filter - sheet/popover", () => {
    it("opens and shows checkboxes for each sport", async () => {
      render(<AvailableParticipantsSection {...defaultProps} />);
      await clickFirstSportFilterTrigger("Filter by sport: All Sports");
      const dialog = screen.getByRole("dialog");
      expect(within(dialog).getByText("NBA")).toBeInTheDocument();
      expect(within(dialog).getByText("NFL")).toBeInTheDocument();
    });

    it("calls onSportFiltersChange with sport added when checkbox clicked", async () => {
      render(<AvailableParticipantsSection {...defaultProps} />);
      await clickFirstSportFilterTrigger("Filter by sport: All Sports");
      const dialog = screen.getByRole("dialog");
      await user.click(within(dialog).getByLabelText("NFL"));
      expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith(["NFL"]);
    });

    it("calls onSportFiltersChange with sport removed when unchecked", async () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          sportFilters={["NFL", "NBA"]}
        />
      );
      await clickFirstSportFilterTrigger("Filter by sport: 2 sports selected");
      const dialog = screen.getByRole("dialog");
      await user.click(within(dialog).getByLabelText("NFL"));
      expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith(["NBA"]);
    });

    it('does not show "Reset" when no filters active', async () => {
      render(<AvailableParticipantsSection {...defaultProps} />);
      await clickFirstSportFilterTrigger("Filter by sport: All Sports");
      const dialog = screen.getByRole("dialog");
      expect(within(dialog).queryByText("Reset")).not.toBeInTheDocument();
    });

    it('shows "Reset" button when filters exist', async () => {
      render(
        <AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
      );
      await clickFirstSportFilterTrigger("Filter by sport: NFL");
      const dialog = screen.getByRole("dialog");
      expect(within(dialog).getByText("Reset")).toBeInTheDocument();
    });

    it('calls onSportFiltersChange([]) and onHideCompletedSportsChange(false) when "Reset" clicked', async () => {
      render(
        <AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
      );
      await clickFirstSportFilterTrigger("Filter by sport: NFL");
      const dialog = screen.getByRole("dialog");
      await user.click(within(dialog).getByText("Reset"));
      expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith([]);
      expect(defaultProps.onHideCompletedSportsChange).toHaveBeenCalledWith(false);
    });

    it("Popover trigger opens and can be closed", async () => {
      render(<AvailableParticipantsSection {...defaultProps} />);
      await clickFirstSportFilterTrigger("Filter by sport: All Sports");
      expect(screen.getByRole("dialog")).toBeInTheDocument();
      await user.keyboard("{Escape}");
      expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
    });
  });

  describe("Watchlist", () => {
    it("does not render watchlist toggle when hasTeam is false", () => {
      render(
        <AvailableParticipantsSection {...defaultProps} hasTeam={false} />
      );
      expect(screen.queryByRole('button', { name: "Add to watchlist" })).not.toBeInTheDocument();
      expect(screen.queryByRole('button', { name: "Remove from watchlist" })).not.toBeInTheDocument();
    });

    it("renders EyeOff icon for unwatched participants when hasTeam is true", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          watchedParticipantIds={new Set<string>()}
        />
      );
      const buttons = screen.getAllByRole('button', { name: "Add to watchlist" });
      expect(buttons.length).toBeGreaterThan(0);
    });

    it("renders EyeOff icon for watched participants", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          watchedParticipantIds={new Set(["1"])}
        />
      );
      const buttons = screen.getAllByRole('button', { name: "Remove from watchlist" });
      expect(buttons.length).toBeGreaterThan(0);
    });

    it("calls onToggleWatchlist when eye button clicked", async () => {
      const onToggleWatchlist = vi.fn();
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          onToggleWatchlist={onToggleWatchlist}
          watchedParticipantIds={new Set<string>()}
        />
      );
      const buttons = screen.getAllByRole('button', { name: "Add to watchlist" });
      await user.click(buttons[0]);
      expect(onToggleWatchlist).toHaveBeenCalledWith("1");
    });

    it("applies emerald highlight to watched participants", () => {
      const { container } = render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          watchedParticipantIds={new Set(["1"])}
        />
      );
      const highlighted = container.querySelector(".bg-emerald-500\\/10");
      expect(highlighted).toBeInTheDocument();
    });

    it("does not highlight unwatched participants", () => {
      const { container } = render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          watchedParticipantIds={new Set<string>()}
        />
      );
      const highlighted = container.querySelector(".bg-emerald-500\\/10");
      expect(highlighted).not.toBeInTheDocument();
    });

    it("shows Watched Only checkbox when hasTeam", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
        />
      );
      expect(screen.getByText("Watched Only")).toBeInTheDocument();
    });

    it("does not show Watched Only checkbox when hasTeam is false", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={false}
        />
      );
      expect(screen.queryByText("Watched Only")).not.toBeInTheDocument();
    });

    it("disables Watched Only checkbox when no participants are watched", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          watchedParticipantIds={new Set<string>()}
        />
      );
      expect(screen.getByLabelText("Watched Only")).toBeDisabled();
    });

    it("calls onShowOnlyWatchedChange when Watched Only checkbox clicked", async () => {
      const onShowOnlyWatchedChange = vi.fn();
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          watchedParticipantIds={new Set(["1"])}
          onShowOnlyWatchedChange={onShowOnlyWatchedChange}
        />
      );
      await user.click(screen.getByLabelText("Watched Only"));
      expect(onShowOnlyWatchedChange).toHaveBeenCalledWith(true);
    });
  });

  describe("Ineligible reasons", () => {
    it("renders inline out-of-flex-spots copy for ineligible participants", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          canPick={true}
          eligibility={{
            eligibleSportIds: new Set(["s2"]),
            ineligibleReasons: {
              s1: createReason("no_flex_spots", "Out of flex spots (2/2)"),
            },
          }}
        />
      );

      expect(screen.getAllByText("Out of flex spots (2/2)").length).toBeGreaterThan(0);
      expect(screen.getAllByTitle("Out of flex spots (2/2)").length).toBeGreaterThan(0);
    });

    it("renders inline no-extras copy for ineligible participants", () => {
      render(
        <AvailableParticipantsSection
          {...defaultProps}
          hasTeam={true}
          canPick={true}
          eligibility={{
            eligibleSportIds: new Set(["s2"]),
            ineligibleReasons: {
              s1: createReason("no_extra_participants", "Not enough NFL participants for remaining teams"),
            },
          }}
        />
      );

      expect(screen.getAllByText("Not enough NFL participants for remaining teams").length).toBeGreaterThan(0);
    });
  });
});
