import { describe, it, expect, vi } from 'vitest';
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ClubHierarchyBuilder } from '../components/founder-portal/club-hierarchy-builder';
import { ClubPermissionMatrix } from '../components/founder-portal/club-permission-matrix';
import { DEFAULT_CLUB_RANKS, DEFAULT_CLUB_CAPABILITIES, DEFAULT_RANK_PERMISSIONS } from '../lib/club-data';
import { ClubMember } from '../components/founder-portal/types';
describe('WEB-13: ClubHierarchyBuilder & ClubPermissionMatrix', () => {
const mockMembers: ClubMember[] = [
{
uid: 'rider-01',
clubId: 'club-test',
status: 'active',
role: 'owner',
displayRank: 'President',
displayName: 'Marcus Vance',
handle: '@marcus_v',
updatedAt: new Date().toISOString(),
},
{
uid: 'rider-02',
clubId: 'club-test',
status: 'active',
role: 'admin',
displayRank: 'Vice President',
displayName: 'Elena Rostova',
handle: '@elena_rides',
updatedAt: new Date().toISOString(),
},
{
uid: 'rider-03',
clubId: 'club-test',
status: 'active',
role: 'member',
displayRank: 'Road Captain',
displayName: 'Dave Miller',
handle: '@dave_m',
updatedAt: new Date().toISOString(),
},
];
it('renders all organizational tiers and rank nodes in hierarchy builder', () => {
const handleSelectRank = vi.fn();
const handleUpdateMemberRank = vi.fn().mockResolvedValue(undefined);
render(
);
expect(screen.getByText('Club Hierarchy Builder')).toBeDefined();
expect(screen.getByText('Tier 1 • Executive Command')).toBeDefined();
expect(screen.getByText('Tier 2 • Leadership & Administration')).toBeDefined();
expect(screen.getByText('Tier 3 • Tactical Operations & Pack Safety')).toBeDefined();
expect(screen.getByText('Tier 4 • Squad Formation & Prospects')).toBeDefined();
expect(screen.getByText('President')).toBeDefined();
expect(screen.getByText('Vice President')).toBeDefined();
expect(screen.getByText('Road Captain')).toBeDefined();
expect(screen.getByText('Tail Gunner')).toBeDefined();
});
it('selects rank node and triggers onSelectRank callback', () => {
const handleSelectRank = vi.fn();
const handleUpdateMemberRank = vi.fn().mockResolvedValue(undefined);
render(
);
const roadCaptainNode = screen.getByText('Road Captain');
fireEvent.click(roadCaptainNode);
expect(handleSelectRank).toHaveBeenCalledWith('road_captain');
});
it('opens assign member modal and executes rank update', async () => {
const handleSelectRank = vi.fn();
const handleUpdateMemberRank = vi.fn().mockResolvedValue(undefined);
render(
);
const assignButtons = screen.getAllByText('Assign Rider');
fireEvent.click(assignButtons[0]);
expect(screen.getByText(/Assign to /)).toBeDefined();
const select = screen.getByRole('combobox');
fireEvent.change(select, { target: { value: 'rider-03' } });
const submitBtn = screen.getByRole('button', { name: /Assign Rank/i });
fireEvent.click(submitBtn);
expect(handleUpdateMemberRank).toHaveBeenCalledWith('rider-03', expect.any(String));
});
it('renders permission matrix capabilities for reference', () => {
const handleToggle = vi.fn();
render(
);
expect(screen.getByText('Operational Permission Matrix')).toBeDefined();
expect(screen.getByText('Plan Club Rides')).toBeDefined();
expect(screen.getByText('Review Applications')).toBeDefined();
expect(screen.getByText('Post Announcements')).toBeDefined();
expect(screen.getByText('Moderate Club Wall')).toBeDefined();
});
// WEB-W1 regression: without a persistence backend the matrix must be an honest
// read-only reference. It must not offer a Save control, must not report a save,
// and must not mutate permissions — even for an Owner/Admin (canEdit=true).
it('is read-only and never reports a false save when persistence is unavailable', () => {
const handleToggle = vi.fn();
const handleSave = vi.fn().mockResolvedValue(undefined);
render(
);
// No save/reset affordances are rendered.
expect(screen.queryByRole('button', { name: /Save Matrix/i })).toBeNull();
expect(screen.queryByRole('button', { name: /Reset Defaults/i })).toBeNull();
expect(screen.queryByText(/Saved/)).toBeNull();
// A truthful view-only indicator and availability notice are present.
expect(screen.getByText(/View-only reference/i)).toBeDefined();
expect(screen.getByText(/not saved yet/i)).toBeDefined();
// Toggles are disabled and do not mutate permissions.
const toggleButton = screen.getByRole('button', {
name: /Standard setting:\s*Plan Club Rides for Road Captain/i,
});
expect((toggleButton as HTMLButtonElement).disabled).toBe(true);
fireEvent.click(toggleButton);
expect(handleToggle).not.toHaveBeenCalled();
expect(handleSave).not.toHaveBeenCalled();
});
// Forward-compatible editable path: when a persistence backend is wired AND the
// caller may edit, the matrix exposes real Save/Reset controls and toggles.
it('allows editing and saving only when persistence is available and canEdit', async () => {
const handleToggle = vi.fn();
const handleReset = vi.fn();
const handleSave = vi.fn().mockResolvedValue(undefined);
render(
);
const toggleButton = screen.getByRole('button', { name: 'Toggle Plan Club Rides for Road Captain' });
fireEvent.click(toggleButton);
expect(handleToggle).toHaveBeenCalledWith('road_captain', 'planRides');
const resetBtn = screen.getByRole('button', { name: /Reset Defaults/i });
fireEvent.click(resetBtn);
expect(handleReset).toHaveBeenCalled();
const saveBtn = screen.getByRole('button', { name: /Save Matrix/i });
fireEvent.click(saveBtn);
expect(handleSave).toHaveBeenCalled();
});
});