import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import * as AuthContext from '@/context/AuthContext';
import * as LicenseContext from '@/context/LicenseContext';
import * as NodeContext from '@/context/NodeContext';
import { SENCHO_NAVIGATE_EVENT } from '@/components/NodeManager';
import { useViewNavigationState } from '../hooks/useViewNavigationState';

vi.mock('@/context/AuthContext');
vi.mock('@/context/LicenseContext');
vi.mock('@/context/NodeContext');

const useExperimentalMock = vi.fn(() => ({ experimental: true, experimentalReady: true }));
vi.mock('@/hooks/useExperimental', () => ({
  useExperimental: () => useExperimentalMock(),
}));

function mockActiveNode(type: 'local' | 'remote' | null) {
  vi.mocked(NodeContext.useNodes).mockReturnValue({
    activeNode: type === null ? null : { type, id: 1, name: 'n' },
  } as unknown as ReturnType<typeof NodeContext.useNodes>);
}

function mockLicense(isPaid: boolean, licenseStatus: 'ready' | 'loading' | 'error' = 'ready') {
  vi.mocked(LicenseContext.useLicense).mockReturnValue({
    isPaid,
    licenseStatus,
  } as unknown as ReturnType<typeof LicenseContext.useLicense>);
}

function mockAuth(
  isAdmin: boolean,
  can: (p: string) => boolean,
  permissionsStatus: 'ready' | 'loading' | 'error' = 'ready',
) {
  vi.mocked(AuthContext.useAuth).mockReturnValue({
    isAdmin,
    can,
    permissionsStatus,
  } as unknown as ReturnType<typeof AuthContext.useAuth>);
}

// Community non-admin with node:read (viewer): Fleet yes, admin-only no.
function mockCommunityUser() {
  mockAuth(false, (p) => p === 'node:read');
  mockLicense(false);
}

// Deployer: stack permissions but no node:read, so no Fleet affordance.
function mockDeployer() {
  mockAuth(false, (p) => p === 'stack:read' || p === 'stack:deploy');
  mockLicense(false);
}

function mockPaidAdmin() {
  mockAuth(
    true,
    (p) => p === 'system:audit' || p === 'system:console' || p === 'node:read' || p === 'stack:deploy' || p === 'node:manage',
  );
  mockLicense(true);
}

// Synthetic gate-isolation helper: omits system:audit so tests can assert the
// Audit hide path. Real Admin always includes system:audit in the permission matrix.
function mockCommunityAdmin() {
  mockAuth(true, (p) => p === 'system:console' || p === 'node:read' || p === 'stack:deploy');
  mockLicense(false);
}

function mockCommunityAdminWithAudit() {
  mockAuth(
    true,
    (p) => p === 'system:audit' || p === 'system:console' || p === 'node:read' || p === 'stack:deploy',
  );
  mockLicense(false);
}

describe('useViewNavigationState', () => {
  beforeEach(() => {
    mockCommunityUser();
    mockActiveNode('local');
    useExperimentalMock.mockReturnValue({ experimental: true, experimentalReady: true });
  });

  // ── initial state ──────────────────────────────────────────────────────────

  it('returns default state on mount', () => {
    const { result } = renderHook(() => useViewNavigationState());
    expect(result.current.activeView).toBe('dashboard');
    expect(result.current.settingsSection).toBe('appearance');
    expect(result.current.filterNodeId).toBeNull();
    expect(result.current.schedulePrefill).toBeNull();
    expect(result.current.mobileNavOpen).toBe(false);
  });

  // ── handleNavigate ─────────────────────────────────────────────────────────

  it('handleNavigate is a no-op when navigating to the current view', () => {
    const onNavigateToDashboard = vi.fn();
    const { result } = renderHook(() =>
      useViewNavigationState({ onNavigateToDashboard }),
    );
    act(() => result.current.handleNavigate('dashboard'));
    expect(onNavigateToDashboard).not.toHaveBeenCalled();
    expect(result.current.activeView).toBe('dashboard');
  });

  it('handleNavigate to dashboard calls onNavigateToDashboard and sets activeView', () => {
    const onNavigateToDashboard = vi.fn();
    const { result } = renderHook(() =>
      useViewNavigationState({ onNavigateToDashboard }),
    );
    // Navigate away first so dashboard→dashboard no-op guard does not fire
    act(() => result.current.handleNavigate('fleet'));
    expect(result.current.activeView).toBe('fleet');

    act(() => result.current.handleNavigate('dashboard'));
    expect(onNavigateToDashboard).toHaveBeenCalledOnce();
    expect(result.current.activeView).toBe('dashboard');
  });

  it('handleNavigate to a non-dashboard view sets activeView and clears filterNodeId', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'fleet', nodeId: 42 } }),
      );
    });
    expect(result.current.filterNodeId).toBe(42);

    act(() => result.current.handleNavigate('resources'));
    expect(result.current.activeView).toBe('resources');
    expect(result.current.filterNodeId).toBeNull();
  });

  // ── handleOpenSettings ─────────────────────────────────────────────────────

  it('handleOpenSettings navigates to settings and clears filterNodeId', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'fleet', nodeId: 7 } }),
      );
    });
    act(() => result.current.handleOpenSettings());
    expect(result.current.activeView).toBe('settings');
    expect(result.current.filterNodeId).toBeNull();
  });

  it('handleOpenSettings with a section updates settingsSection', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => result.current.handleOpenSettings('nodes'));
    expect(result.current.settingsSection).toBe('nodes');
    expect(result.current.activeView).toBe('settings');
  });

  it('handleOpenSettings without a section does not change settingsSection', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => result.current.handleOpenSettings('labels'));
    act(() => result.current.handleOpenSettings());
    expect(result.current.settingsSection).toBe('labels');
    expect(result.current.activeView).toBe('settings');
  });

  // ── handlePrefillConsumed ──────────────────────────────────────────────────

  it('handlePrefillConsumed clears schedulePrefill', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => result.current.setSchedulePrefill({ stackName: 'web.yml', nodeId: 1 }));
    expect(result.current.schedulePrefill).toEqual({ stackName: 'web.yml', nodeId: 1 });
    act(() => result.current.handlePrefillConsumed());
    expect(result.current.schedulePrefill).toBeNull();
  });

  // ── SENCHO_NAVIGATE_EVENT ──────────────────────────────────────────────────

  it('SENCHO_NAVIGATE_EVENT sets activeView and filterNodeId', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'fleet', nodeId: 5 } }),
      );
    });
    expect(result.current.activeView).toBe('fleet');
    expect(result.current.filterNodeId).toBe(5);
  });

  it('SENCHO_NAVIGATE_EVENT to fleet sets fleetTab then activeView (deep-link, no race)', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'fleet', fleetTab: 'snapshots' } }),
      );
    });
    expect(result.current.activeView).toBe('fleet');
    expect(result.current.fleetActiveTab).toBe('snapshots');
  });

  it('SENCHO_NAVIGATE_EVENT with no nodeId sets filterNodeId to null', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'fleet', nodeId: 9 } }),
      );
    });
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'resources' } }),
      );
    });
    expect(result.current.filterNodeId).toBeNull();
  });

  it('cleans up SENCHO_NAVIGATE_EVENT listener on unmount', () => {
    const { result, unmount } = renderHook(() => useViewNavigationState());
    unmount();
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'fleet' } }),
      );
    });
    expect(result.current.activeView).toBe('dashboard');
  });

  // ── navItems: community user ───────────────────────────────────────────────

  it('navItems for community non-admin user contains base items only and hides admin-only Logs', () => {
    const { result } = renderHook(() => useViewNavigationState());
    const values = result.current.navItems.map(i => i.value);
    expect(values).toContain('dashboard');
    expect(values).toContain('fleet');
    expect(values).toContain('resources');
    expect(values).toContain('templates');
    // Networking is a base nav item; the global search palette's Pages group
    // derives from this list, so it needs no separate registration.
    expect(values).toContain('networking');
    // Logs is an admin-only operator view; a non-admin must not see the entry.
    expect(values).not.toContain('global-observability');
    expect(values).not.toContain('auto-updates');
    expect(values).not.toContain('host-console');
    expect(values).not.toContain('audit-log');
    expect(values).not.toContain('scheduled-ops');
  });

  it('hides Fleet from a user without node:read (deployer)', () => {
    mockDeployer();
    const { result } = renderHook(() => useViewNavigationState());
    const values = result.current.navItems.map(i => i.value);
    expect(values).not.toContain('fleet');
    // The other base items remain reachable.
    expect(values).toContain('dashboard');
    expect(values).toContain('resources');
    expect(values).toContain('templates');
  });

  it('redirects a user without node:read off the Fleet view reached via a deep-link event', () => {
    const onNavigateToDashboard = vi.fn();
    mockDeployer();
    const { result } = renderHook(() => useViewNavigationState({ onNavigateToDashboard }));
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'fleet' } }),
      );
    });
    expect(result.current.activeView).toBe('dashboard');
    expect(onNavigateToDashboard).toHaveBeenCalled();
  });

  it('shows the admin-only Logs entry for an admin on any tier (role gate, not tier gate)', () => {
    mockCommunityAdmin();
    const { result } = renderHook(() => useViewNavigationState());
    expect(result.current.navItems.map(i => i.value)).toContain('global-observability');
  });

  it('hides Audit for a community admin without system:audit', () => {
    mockCommunityAdmin();
    const { result } = renderHook(() => useViewNavigationState());
    const values = result.current.navItems.map(i => i.value);
    expect(values).toContain('auto-updates');
    expect(values).toContain('scheduled-ops');
    expect(values).toContain('host-console');
    expect(values).not.toContain('audit-log');
    // The auto-updates nav item surfaces under the short label "Update".
    expect(result.current.navItems.find(i => i.value === 'auto-updates')?.label).toBe('Update');
  });

  it('shows Audit for a community admin with system:audit and keeps deep-links', () => {
    mockCommunityAdminWithAudit();
    const onNavigateToDashboard = vi.fn();
    const { result } = renderHook(() =>
      useViewNavigationState({ onNavigateToDashboard }),
    );
    expect(result.current.navItems.map((i) => i.value)).toContain('audit-log');

    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'audit-log' } }),
      );
    });
    expect(result.current.activeView).toBe('audit-log');
    expect(onNavigateToDashboard).not.toHaveBeenCalled();
  });

  it('does not normalize audit-log away while permissions are still loading', () => {
    mockAuth(true, () => false, 'loading');
    mockLicense(false);

    const onNavigateToDashboard = vi.fn();
    const { result } = renderHook(() =>
      useViewNavigationState({ onNavigateToDashboard }),
    );
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'audit-log' } }),
      );
    });
    expect(result.current.activeView).toBe('audit-log');
    expect(onNavigateToDashboard).not.toHaveBeenCalled();
  });

  it('redirects a user without system:audit off the Audit view reached via a deep-link event', () => {
    const onNavigateToDashboard = vi.fn();
    mockCommunityAdmin();
    const { result } = renderHook(() =>
      useViewNavigationState({ onNavigateToDashboard }),
    );
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'audit-log' } }),
      );
    });
    expect(result.current.activeView).toBe('dashboard');
    expect(onNavigateToDashboard).toHaveBeenCalled();
  });

  it('redirects a non-admin off the Logs view when reached via a deep-link event', () => {
    const onNavigateToDashboard = vi.fn();
    // Community (non-admin) is the beforeEach default.
    const { result } = renderHook(() => useViewNavigationState({ onNavigateToDashboard }));
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'global-observability' } }),
      );
    });
    expect(result.current.activeView).toBe('dashboard');
    expect(onNavigateToDashboard).toHaveBeenCalled();
  });

  // ── navItems: paid admin ───────────────────────────────────────────────────

  it('navItems for a paid admin contains all items', () => {
    mockPaidAdmin();
    const { result } = renderHook(() => useViewNavigationState());
    const values = result.current.navItems.map(i => i.value);
    expect(values).toContain('auto-updates');
    expect(values).toContain('host-console');
    expect(values).toContain('audit-log');
    expect(values).toContain('scheduled-ops');
  });

  // ── navItems: hub-only gating on remote node ───────────────────────────────

  it('hides hub-only views from the nav strip when active node is remote', () => {
    mockPaidAdmin();
    mockActiveNode('remote');
    const { result } = renderHook(() => useViewNavigationState());
    const values = result.current.navItems.map(i => i.value);
    expect(values).not.toContain('fleet');
    expect(values).not.toContain('scheduled-ops');
    expect(values).not.toContain('audit-log');
    expect(values).not.toContain('global-observability');
    expect(values).not.toContain('auto-updates');
    // Node-level views remain visible.
    expect(values).toContain('dashboard');
    expect(values).toContain('resources');
    expect(values).toContain('templates');
    expect(values).toContain('host-console');
  });

  it('shows hub-only views again when active node switches back to local', () => {
    mockPaidAdmin();
    mockActiveNode('remote');
    const { result, rerender } = renderHook(() => useViewNavigationState());
    expect(result.current.navItems.map(i => i.value)).not.toContain('fleet');

    mockActiveNode('local');
    rerender();
    const values = result.current.navItems.map(i => i.value);
    expect(values).toContain('fleet');
    expect(values).toContain('scheduled-ops');
    expect(values).toContain('audit-log');
  });

  // ── auto-redirect when on a hub-only view and node switches to remote ──────

  it('auto-redirects to dashboard when active view is hub-only and node becomes remote', () => {
    const onNavigateToDashboard = vi.fn();
    mockPaidAdmin();
    mockActiveNode('local');
    const { result, rerender } = renderHook(() =>
      useViewNavigationState({ onNavigateToDashboard }),
    );

    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'fleet', nodeId: 7 } }),
      );
    });
    expect(result.current.activeView).toBe('fleet');
    expect(result.current.filterNodeId).toBe(7);

    mockActiveNode('remote');
    rerender();

    expect(result.current.activeView).toBe('dashboard');
    expect(result.current.filterNodeId).toBeNull();
    expect(onNavigateToDashboard).toHaveBeenCalledOnce();
  });

  it('does not redirect when a non-hub-only view is active and node becomes remote', () => {
    const onNavigateToDashboard = vi.fn();
    mockPaidAdmin();
    mockActiveNode('local');
    const { result, rerender } = renderHook(() =>
      useViewNavigationState({ onNavigateToDashboard }),
    );

    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'resources' } }),
      );
    });
    expect(result.current.activeView).toBe('resources');

    mockActiveNode('remote');
    rerender();

    expect(result.current.activeView).toBe('resources');
    expect(onNavigateToDashboard).not.toHaveBeenCalled();
  });

  // ── Security view: node-scoped, deep-linkable tab ──────────────────────────

  it('includes the Security nav item for a community user', () => {
    const { result } = renderHook(() => useViewNavigationState());
    expect(result.current.navItems.map(i => i.value)).toContain('security');
  });

  it('keeps Security visible on a remote node (node-scoped, not hub-only)', () => {
    mockActiveNode('remote');
    const { result } = renderHook(() => useViewNavigationState());
    expect(result.current.navItems.map(i => i.value)).toContain('security');
  });

  it('defaults securityTab to overview', () => {
    const { result } = renderHook(() => useViewNavigationState());
    expect(result.current.securityTab).toBe('overview');
  });

  it('navigate to security with a tab sets securityTab then activeView (deep-link, no race)', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'security', tab: 'history', nodeId: 4 } }),
      );
    });
    expect(result.current.activeView).toBe('security');
    expect(result.current.securityTab).toBe('history');
    expect(result.current.filterNodeId).toBe(4);
  });

  it('navigate to security without a tab defaults securityTab to overview', () => {
    const { result } = renderHook(() => useViewNavigationState());
    act(() => result.current.setSecurityTab('history'));
    act(() => {
      window.dispatchEvent(
        new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'security' } }),
      );
    });
    expect(result.current.activeView).toBe('security');
    expect(result.current.securityTab).toBe('overview');
  });

  // ── host-console discovery (no longer experimental) ────────────────────────

  it('keeps Console in nav for a paid admin when experimental discovery is off', () => {
    mockPaidAdmin();
    useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
    const { result } = renderHook(() => useViewNavigationState());
    expect(result.current.navItems.map(i => i.value)).toContain('host-console');
  });

  it('keeps Console in nav while experimental metadata is still loading', () => {
    mockPaidAdmin();
    useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: false });
    const { result } = renderHook(() => useViewNavigationState());
    expect(result.current.navItems.map(i => i.value)).toContain('host-console');
  });

  it('keeps a host-console deep link selected when experimental is off', () => {
    mockPaidAdmin();
    useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
    const onNavigateToDashboard = vi.fn();
    const { result } = renderHook(() => useViewNavigationState({ onNavigateToDashboard }));
    act(() => result.current.setActiveView('host-console'));
    expect(result.current.activeView).toBe('host-console');
    expect(onNavigateToDashboard).not.toHaveBeenCalled();
  });
});
