import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { screen, fireEvent, waitFor } from "@testing-library/react";
import { renderWithProviders } from "../test/helpers";
import { BandSettingsPage } from "./BandSettingsPage";

// ── Shared fixtures ───────────────────────────────────────────────────────────

const ME = { id: "m-me", email: "s@example.com", display_name: "Steffen", avatar_url: null, created_at: "" };

const BAND = {
  id: "band-1",
  name: "Loud Hands",
  slug: "loud-hands",
  genre_tags: ["post-rock", "math-rock"],
  nc_folder_path: "bands/loud-hands/",
};

const MEMBERS_ADMIN = [
  { id: "m-me", display_name: "Steffen", email: "s@example.com", role: "admin", joined_at: "" },
  { id: "m-2", display_name: "Alex", email: "a@example.com", role: "member", joined_at: "" },
];

const MEMBERS_NON_ADMIN = [
  { id: "m-me", display_name: "Steffen", email: "s@example.com", role: "member", joined_at: "" },
  { id: "m-2", display_name: "Alex", email: "a@example.com", role: "admin", joined_at: "" },
];

const INVITES_RESPONSE = {
  invites: [
    {
      id: "inv-1",
      token: "abcdef1234567890abcd",
      role: "member",
      expires_at: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
      is_used: false,
      band_id: "band-1",
      created_at: new Date().toISOString(),
      used_at: null,
    },
  ],
  total: 1,
  pending: 1,
};

// ── Mocks ─────────────────────────────────────────────────────────────────────

const {
  mockGetBand,
  mockApiGet,
  mockApiPost,
  mockApiPatch,
  mockApiDelete,
  mockListInvites,
  mockRevokeInvite,
} = vi.hoisted(() => ({
  mockGetBand: vi.fn(),
  mockApiGet: vi.fn(),
  mockApiPost: vi.fn(),
  mockApiPatch: vi.fn(),
  mockApiDelete: vi.fn(),
  mockListInvites: vi.fn(),
  mockRevokeInvite: vi.fn(),
}));

vi.mock("../api/bands", () => ({ getBand: mockGetBand }));
vi.mock("../api/invites", () => ({
  listInvites: mockListInvites,
  revokeInvite: mockRevokeInvite,
}));
vi.mock("../api/client", () => ({
  api: {
    get: mockApiGet,
    post: mockApiPost,
    patch: mockApiPatch,
    delete: mockApiDelete,
  },
  isLoggedIn: vi.fn().mockReturnValue(true),
}));

// ── Default mock implementations ─────────────────────────────────────────────

afterEach(() => vi.clearAllMocks());

function setupApiGet(members: typeof MEMBERS_ADMIN) {
  mockApiGet.mockImplementation((url: string) => {
    if (url === "/auth/me") return Promise.resolve(ME);
    if (url.includes("/members")) return Promise.resolve(members);
    return Promise.resolve([]);
  });
}

beforeEach(() => {
  mockGetBand.mockResolvedValue(BAND);
  setupApiGet(MEMBERS_ADMIN);
  mockApiPost.mockResolvedValue({ id: "inv-new", token: "newtoken123", role: "member", expires_at: "" });
  mockApiPatch.mockResolvedValue(BAND);
  mockApiDelete.mockResolvedValue({});
  mockListInvites.mockResolvedValue(INVITES_RESPONSE);
  mockRevokeInvite.mockResolvedValue({});
});

// ── Render helpers ────────────────────────────────────────────────────────────

function renderPanel(panel: "members" | "storage" | "band", members = MEMBERS_ADMIN) {
  setupApiGet(members);
  return renderWithProviders(<BandSettingsPage />, {
    path: "/bands/:bandId/settings/:panel",
    route: `/bands/band-1/settings/${panel}`,
  });
}

// ── Routing ───────────────────────────────────────────────────────────────────

describe("BandSettingsPage — routing (TC-15 to TC-17)", () => {
  it("TC-15: renders Storage panel for /settings/storage", async () => {
    renderPanel("storage");
    const heading = await screen.findByRole("heading", { name: /storage/i });
    expect(heading).toBeTruthy();
  });

  it("TC-16: renders Band Settings panel for /settings/band", async () => {
    renderPanel("band");
    const heading = await screen.findByRole("heading", { name: /band settings/i });
    expect(heading).toBeTruthy();
  });

  it("TC-17: unknown panel falls back to Members", async () => {
    mockApiGet.mockResolvedValue(MEMBERS_ADMIN);
    renderWithProviders(<BandSettingsPage />, {
      path: "/bands/:bandId/settings/:panel",
      route: "/bands/band-1/settings/unknown-panel",
    });
    const heading = await screen.findByRole("heading", { name: /members/i });
    expect(heading).toBeTruthy();
  });
});

// ── Members panel — access control ───────────────────────────────────────────

describe("BandSettingsPage — Members panel access control (TC-18 to TC-23)", () => {
  it("TC-18: admin sees + Invite button", async () => {
    renderPanel("members", MEMBERS_ADMIN);
    const btn = await screen.findByText(/\+ invite/i);
    expect(btn).toBeTruthy();
  });

  it("TC-19: non-admin does not see + Invite button", async () => {
    renderPanel("members", MEMBERS_NON_ADMIN);
    await screen.findByText("Alex"); // wait for members to load
    expect(screen.queryByText(/\+ invite/i)).toBeNull();
  });

  it("TC-20: admin sees Remove button on non-admin members", async () => {
    renderPanel("members", MEMBERS_ADMIN);
    const removeBtn = await screen.findByText("Remove");
    expect(removeBtn).toBeTruthy();
  });

  it("TC-21: non-admin does not see any Remove button", async () => {
    renderPanel("members", MEMBERS_NON_ADMIN);
    await screen.findByText("Alex");
    expect(screen.queryByText("Remove")).toBeNull();
  });

  it("TC-22: admin does not see Remove on admin-role members", async () => {
    renderPanel("members", MEMBERS_ADMIN);
    await screen.findByText("Steffen");
    // Only one Remove button — for Alex (member), not Steffen (admin)
    const removeBtns = screen.queryAllByText("Remove");
    expect(removeBtns).toHaveLength(1);
  });

  it("TC-23: Pending Invites section hidden from non-admins", async () => {
    renderPanel("members", MEMBERS_NON_ADMIN);
    await screen.findByText("Alex");
    expect(screen.queryByText(/pending invites/i)).toBeNull();
  });
});

// ── Members panel — functionality ─────────────────────────────────────────────

describe("BandSettingsPage — Members panel functionality (TC-24 to TC-28)", () => {
  it("TC-24: generate invite shows link in UI", async () => {
    const token = "tok123abc456def789gh";
    mockApiPost.mockResolvedValue({ id: "inv-new", token, role: "member", expires_at: "" });
    renderPanel("members", MEMBERS_ADMIN);
    const inviteBtn = await screen.findByText(/\+ invite/i);
    fireEvent.click(inviteBtn);
    const linkEl = await screen.findByText(new RegExp(token));
    expect(linkEl).toBeTruthy();
  });

  it("TC-26: remove member calls DELETE endpoint", async () => {
    renderPanel("members", MEMBERS_ADMIN);
    const removeBtn = await screen.findByText("Remove");
    fireEvent.click(removeBtn);
    await waitFor(() => {
      expect(mockApiDelete).toHaveBeenCalledWith("/bands/band-1/members/m-2");
    });
  });

  it("TC-27: revoke invite calls revokeInvite and refetches", async () => {
    renderPanel("members", MEMBERS_ADMIN);
    const revokeBtn = await screen.findByText("Revoke");
    fireEvent.click(revokeBtn);
    await waitFor(() => {
      expect(mockRevokeInvite).toHaveBeenCalledWith("inv-1");
    });
  });
});

// ── Storage panel — access control ───────────────────────────────────────────

describe("BandSettingsPage — Storage panel access control (TC-29 to TC-33)", () => {
  it("TC-29: admin sees Edit button", async () => {
    renderPanel("storage", MEMBERS_ADMIN);
    const edit = await screen.findByText("Edit");
    expect(edit).toBeTruthy();
  });

  it("TC-30: non-admin does not see Edit button", async () => {
    renderPanel("storage", MEMBERS_NON_ADMIN);
    await screen.findByText(/scan path/i);
    expect(screen.queryByText("Edit")).toBeNull();
  });

  it("TC-31: saving NC folder path calls PATCH and closes form", async () => {
    renderPanel("storage", MEMBERS_ADMIN);
    fireEvent.click(await screen.findByText("Edit"));
    const input = screen.getByPlaceholderText(/bands\/loud-hands\//i);
    fireEvent.change(input, { target: { value: "bands/custom-path/" } });
    fireEvent.click(screen.getByText("Save"));
    await waitFor(() => {
      expect(mockApiPatch).toHaveBeenCalledWith(
        "/bands/band-1",
        { nc_folder_path: "bands/custom-path/" }
      );
    });
  });

  it("TC-32: cancel edit closes form without calling PATCH", async () => {
    renderPanel("storage", MEMBERS_ADMIN);
    fireEvent.click(await screen.findByText("Edit"));
    fireEvent.click(screen.getByText("Cancel"));
    await waitFor(() => {
      expect(mockApiPatch).not.toHaveBeenCalled();
    });
    expect(screen.queryByText("Save")).toBeNull();
  });

  it("TC-33: shows default path when nc_folder_path is null", async () => {
    mockGetBand.mockResolvedValueOnce({ ...BAND, nc_folder_path: null });
    renderPanel("storage", MEMBERS_ADMIN);
    const path = await screen.findByText("bands/loud-hands/");
    expect(path).toBeTruthy();
  });
});

// ── Band settings panel — access control ──────────────────────────────────────

describe("BandSettingsPage — Band Settings panel access control (TC-34 to TC-40)", () => {
  it("TC-34: admin sees Save changes button", async () => {
    renderPanel("band", MEMBERS_ADMIN);
    const btn = await screen.findByText(/save changes/i);
    expect(btn).toBeTruthy();
  });

  it("TC-35: non-admin sees info text instead of Save button", async () => {
    renderPanel("band", MEMBERS_NON_ADMIN);
    // Wait for the band panel heading so we know the page has fully loaded
    await screen.findByRole("heading", { name: /band settings/i });
    // Once queries settle, the BandPanel-level info text should appear and Save should be absent
    await waitFor(() => {
      expect(screen.getByText(/only admins can edit band settings/i)).toBeTruthy();
    });
    expect(screen.queryByText(/save changes/i)).toBeNull();
  });

  it("TC-36: name field is disabled for non-admins", async () => {
    renderPanel("band", MEMBERS_NON_ADMIN);
    const input = await screen.findByDisplayValue("Loud Hands");
    expect((input as HTMLInputElement).disabled).toBe(true);
  });

  it("TC-37: saving calls PATCH with name and genre_tags", async () => {
    renderPanel("band", MEMBERS_ADMIN);
    await screen.findByText(/save changes/i);
    fireEvent.click(screen.getByText(/save changes/i));
    await waitFor(() => {
      expect(mockApiPatch).toHaveBeenCalledWith("/bands/band-1", {
        name: "Loud Hands",
        genre_tags: ["post-rock", "math-rock"],
      });
    });
  });

  it("TC-38: adding a genre tag shows the new pill", async () => {
    renderPanel("band", MEMBERS_ADMIN);
    const tagInput = await screen.findByPlaceholderText(/add genre tag/i);
    fireEvent.change(tagInput, { target: { value: "punk" } });
    fireEvent.keyDown(tagInput, { key: "Enter" });
    expect(screen.getByText("punk")).toBeTruthy();
  });

  it("TC-39: removing a genre tag removes its pill", async () => {
    renderPanel("band", MEMBERS_ADMIN);
    // Find the × button next to "post-rock"
    await screen.findByText("post-rock");
    // There are two tags; find the × buttons
    const removeButtons = screen.getAllByText("×");
    fireEvent.click(removeButtons[0]);
    expect(screen.queryByText("post-rock")).toBeNull();
  });

  it("TC-40: Delete band button is disabled for non-admins", async () => {
    renderPanel("band", MEMBERS_NON_ADMIN);
    const deleteBtn = await screen.findByText(/delete band/i);
    expect((deleteBtn as HTMLButtonElement).disabled).toBe(true);
  });
});
