/**
 * CMS Page Editor Tests
 *
 * Tests the BEHAVIOR of the page editor:
 * - Loading and error states
 * - Settings panel toggle and fields
 * - Save and publish flow
 * - Success/error message display
 * - Unsaved changes tracking
 *
 * Philosophy: Test what the user experiences, not implementation details.
 * Puck editor is mocked since it's a third-party component with its own tests.
 */

import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Mock next/navigation
const mockParams = { id: 'page-123' };
jest.mock('next/navigation', () => ({
  useParams: () => mockParams,
}));

// Mock next-auth
const mockSession = {
  data: { user: { id: 'user-123', email: 'test@example.com' } },
  status: 'authenticated' as const,
};
jest.mock('next-auth/react', () => ({
  useSession: () => mockSession,
}));

// Mock next/dynamic to return our mock Puck component
jest.mock('next/dynamic', () => {
  return function mockDynamic(
    _importFn: () => Promise<{ Puck: React.ComponentType<unknown> }>,
    _options?: { loading?: () => React.ReactNode }
  ) {
    // Return a mock Puck component
    const MockPuck = ({
      data,
      onChange,
      onPublish,
      headerTitle,
    }: {
      data: unknown;
      onChange: (data: unknown) => void;
      onPublish: (data: unknown) => void;
      headerTitle: string;
    }) => (
      <div data-testid="puck-editor">
        <div data-testid="puck-header-title">{headerTitle}</div>
        <button data-testid="puck-save-btn" onClick={() => onPublish(data)}>
          Save via Puck
        </button>
        <button
          data-testid="puck-change-btn"
          onClick={() => onChange({ ...(data as object), modified: true })}
        >
          Trigger Change
        </button>
      </div>
    );

    // Support the loading option
    MockPuck.displayName = 'MockPuck';
    return MockPuck;
  };
});

// Mock next/image
jest.mock('next/image', () => ({
  __esModule: true,
  // eslint-disable-next-line @next/next/no-img-element
  default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
}));

// Mock next/link
jest.mock('next/link', () => ({
  __esModule: true,
  default: ({ href, children, ...props }: { href: string; children: React.ReactNode }) => (
    <a href={href} {...props}>
      {children}
    </a>
  ),
}));

// Mock puck-config
jest.mock('@/lib/cms/puck-config', () => ({
  puckConfig: {},
}));

// Mock UI components
jest.mock('@/components/ui/Card', () => ({
  Card: ({ children }: { children: React.ReactNode }) => <div className="card">{children}</div>,
  CardBody: ({ children }: { children: React.ReactNode }) => (
    <div className="card-body">{children}</div>
  ),
}));

jest.mock('@/components/ui/Button', () => ({
  __esModule: true,
  default: ({
    children,
    onClick,
    disabled,
    ...props
  }: {
    children: React.ReactNode;
    onClick?: () => void;
    disabled?: boolean;
  }) => (
    <button onClick={onClick} disabled={disabled} {...props}>
      {children}
    </button>
  ),
}));

// Import after mocks
import PageEditorPage from '../page';

describe('CMS Page Editor', () => {
  const mockPageData = {
    page: {
      id: 'page-123',
      title: 'Test Page',
      slug: 'test-page',
      type: 'page' as const,
      status: 'draft' as const,
      puckData: { content: [], root: { props: { title: 'Test Page' } } },
      metaTitle: 'SEO Title',
      metaDescription: 'SEO Description',
      excerpt: null,
      author: null,
      publishedAt: null,
      updatedAt: '2024-01-01T00:00:00Z',
    },
  };

  const mockDealerData = {
    id: 'dealer-123',
    status: 'active',
    subdomain: 'testdealer',
    domain: 'com',
    businessName: 'Test Dealer LLC',
    contactName: 'John Dealer',
    phone: '(555) 123-4567',
    address: '123 Main St',
    city: 'Minneapolis',
    state: 'MN',
    zip: '55401',
    contactEmail: 'test@dealer.com',
  };

  const mockNavigationData = {
    items: [
      {
        id: 'nav-1',
        label: 'Home',
        pageId: null,
        externalUrl: null,
        parentId: null,
        sortOrder: 0,
        openInNewTab: false,
        page: null,
      },
    ],
  };

  beforeEach(() => {
    jest.clearAllMocks();
    mockSession.status = 'authenticated';
    mockSession.data = { user: { id: 'user-123', email: 'test@example.com' } };
    mockParams.id = 'page-123';

    // Default successful API responses
    global.fetch = jest.fn().mockImplementation((url: string) => {
      if (url === '/api/cms/pages/page-123') {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockPageData),
        });
      }
      if (url === '/api/dealer') {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockDealerData),
        });
      }
      if (url === '/api/cms/navigation') {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockNavigationData),
        });
      }
      return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
    });
  });

  describe('Loading States', () => {
    it('shows loading message while fetching page', async () => {
      // Delay the API response
      (global.fetch as jest.Mock).mockImplementation(
        () =>
          new Promise((resolve) => setTimeout(() => resolve({ ok: true, json: () => ({}) }), 100))
      );

      render(<PageEditorPage />);

      expect(screen.getByText(/loading page/i)).toBeInTheDocument();
    });

    it('shows loading while session is loading', () => {
      mockSession.status = 'loading' as 'authenticated';

      render(<PageEditorPage />);

      expect(screen.getByText(/loading page/i)).toBeInTheDocument();
    });
  });

  describe('Error States', () => {
    it('displays error message when page fetch fails', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: false,
            json: () => Promise.resolve({ error: 'Page not found' }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByText(/page not found/i)).toBeInTheDocument();
      });
    });

    it('provides Back to Pages link on error', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: false,
            json: () => Promise.resolve({ error: 'Server error' }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('link', { name: /back to pages/i })).toBeInTheDocument();
      });
    });
  });

  describe('Editor UI', () => {
    it('displays page title in editor', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByTestId('puck-editor')).toBeInTheDocument();
      });

      expect(screen.getByTestId('puck-header-title')).toHaveTextContent('Test Page');
    });

    it('shows Back link to pages list', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByTestId('puck-editor')).toBeInTheDocument();
      });

      expect(screen.getByRole('link', { name: /back/i })).toHaveAttribute(
        'href',
        '/dashboard/cms/pages'
      );
    });

    it('shows Settings button', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });
    });

    it('shows Publish button', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });
    });

    it('shows View Page link for active dealers with published pages', async () => {
      // Update mock to return published page
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                page: { ...mockPageData.page, status: 'published' },
              }),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve({ ...mockDealerData, status: 'active' }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('link', { name: /view page/i })).toBeInTheDocument();
      });
    });

    it('hides View Page link for draft pages', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByTestId('puck-editor')).toBeInTheDocument();
      });

      expect(screen.queryByRole('link', { name: /view page/i })).not.toBeInTheDocument();
    });
  });

  describe('View Page link URL — uses dealer.domainPrefix for prod path', () => {
    // Jest runs with NODE_ENV='test' (not 'development'), so the production URL
    // branch of the View Page href is exercised by default. The prod path must
    // use `dealer.domainPrefix` (never hardcoded `myamsoil`) or a shopamsoil
    // dealer's "View Page" link would point at their sibling tenant's site.
    it('builds the prod URL using dealer.domainPrefix for a shopamsoil page', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                page: { ...mockPageData.page, status: 'published' },
              }),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                ...mockDealerData,
                status: 'active',
                domainPrefix: 'shopamsoil',
              }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('link', { name: /view page/i })).toBeInTheDocument();
      });

      // Must be shopamsoil (never the hardcoded myamsoil) and path = slug only for page type
      expect(screen.getByRole('link', { name: /view page/i })).toHaveAttribute(
        'href',
        'https://testdealer.shopamsoil.com/test-page'
      );
    });

    it('builds the prod URL with blog/ prefix for a shopamsoil blog post', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                page: {
                  ...mockPageData.page,
                  type: 'blog',
                  status: 'published',
                },
              }),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                ...mockDealerData,
                status: 'active',
                domainPrefix: 'shopamsoil',
              }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('link', { name: /view page/i })).toBeInTheDocument();
      });

      expect(screen.getByRole('link', { name: /view page/i })).toHaveAttribute(
        'href',
        'https://testdealer.shopamsoil.com/blog/test-page'
      );
    });
  });

  describe('Settings Panel', () => {
    it('toggles settings panel visibility', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      // Settings should be hidden initially
      expect(screen.queryByDisplayValue('Test Page')).not.toBeInTheDocument();

      // Click to show settings
      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      // Title field should now be visible
      expect(screen.getByDisplayValue('Test Page')).toBeInTheDocument();
    });

    it('displays page title in settings field', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      expect(screen.getByDisplayValue('Test Page')).toBeInTheDocument();
    });

    it('displays page slug in settings field', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      expect(screen.getByDisplayValue('test-page')).toBeInTheDocument();
    });

    it('displays meta title in settings field', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      expect(screen.getByDisplayValue('SEO Title')).toBeInTheDocument();
    });

    it('sanitizes slug input to lowercase alphanumeric with hyphens', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      const slugInput = screen.getByDisplayValue('test-page');
      await userEvent.clear(slugInput);
      await userEvent.type(slugInput, 'My Page Title!');

      // Should be sanitized to lowercase alphanumeric with hyphens
      expect(slugInput).toHaveValue('mypagetitle');
    });

    it('shows author field for blog type pages', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                page: { ...mockPageData.page, type: 'blog', author: 'John Doe' },
              }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      expect(screen.getByDisplayValue('John Doe')).toBeInTheDocument();
    });

    it('hides author field for regular pages', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      expect(screen.queryByPlaceholderText(/author name/i)).not.toBeInTheDocument();
    });
  });

  describe('Publish Flow', () => {
    it('calls PUT API when Publish is clicked', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: true,
              json: () => Promise.resolve({ page: { ...mockPageData.page, status: 'published' } }),
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        if (url === '/api/dealers/dealer-123/publish') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve({ success: true }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(global.fetch).toHaveBeenCalledWith(
          '/api/cms/pages/page-123',
          expect.objectContaining({
            method: 'PUT',
            body: expect.stringContaining('"status":"published"'),
          })
        );
      });
    });

    it('calls dealer publish API after saving page', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: true,
              json: () => Promise.resolve({ page: { ...mockPageData.page, status: 'published' } }),
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        if (url === '/api/dealers/dealer-123/publish') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve({ success: true }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(global.fetch).toHaveBeenCalledWith(
          '/api/dealers/dealer-123/publish',
          expect.objectContaining({ method: 'POST' })
        );
      });
    });

    it('shows success message on successful publish', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: true,
              json: () => Promise.resolve({ page: { ...mockPageData.page, status: 'published' } }),
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        if (url === '/api/dealers/dealer-123/publish') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve({ success: true }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByText(/published successfully/i)).toBeInTheDocument();
      });
    });

    it('shows error message on publish failure', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: false,
              json: () => Promise.resolve({ error: 'Validation failed' }),
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByText(/validation failed/i)).toBeInTheDocument();
      });
    });

    it('shows Publishing... during operation', async () => {
      let resolvePublish: () => void;
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return new Promise((resolve) => {
              resolvePublish = () =>
                resolve({
                  ok: true,
                  json: () => Promise.resolve({ page: mockPageData.page }),
                });
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publishing/i })).toBeInTheDocument();
      });

      // Cleanup
      resolvePublish!();
    });

    it('disables Publish button while publishing', async () => {
      let resolvePublish: () => void;
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return new Promise((resolve) => {
              resolvePublish = () =>
                resolve({
                  ok: true,
                  json: () => Promise.resolve({ page: mockPageData.page }),
                });
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publishing/i })).toBeDisabled();
      });

      // Cleanup
      resolvePublish!();
    });

    it('shows field validation errors from API', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: false,
              json: () =>
                Promise.resolve({
                  fieldErrors: {
                    title: ['Title is required'],
                    slug: ['Slug already exists'],
                  },
                }),
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByText(/title is required/i)).toBeInTheDocument();
      });
    });
  });

  describe('Puck Editor Integration', () => {
    it('passes page title to Puck as headerTitle', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByTestId('puck-header-title')).toHaveTextContent('Test Page');
      });
    });

    it('can trigger save via Puck onPublish callback', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: true,
              json: () => Promise.resolve({ page: mockPageData.page }),
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByTestId('puck-save-btn')).toBeInTheDocument();
      });

      await userEvent.click(screen.getByTestId('puck-save-btn'));

      await waitFor(() => {
        expect(global.fetch).toHaveBeenCalledWith(
          '/api/cms/pages/page-123',
          expect.objectContaining({ method: 'PUT' })
        );
      });
    });
  });

  describe('Error Banner', () => {
    it('displays error message from failed publish', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: false,
              json: () => Promise.resolve({ error: 'Validation error' }),
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      // Error should be displayed (either in banner or error view)
      await waitFor(() => {
        expect(screen.getByText(/validation error/i)).toBeInTheDocument();
      });
    });
  });

  describe('Success Banner', () => {
    it('can dismiss success banner', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: true,
              json: () => Promise.resolve({ page: { ...mockPageData.page, status: 'published' } }),
            });
          }
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockPageData),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockDealerData),
          });
        }
        if (url === '/api/dealers/dealer-123/publish') {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve({ success: true }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByText(/published successfully/i)).toBeInTheDocument();
      });

      // Click dismiss button (the ✕)
      const dismissButton = screen.getByRole('button', { name: /✕/ });
      await userEvent.click(dismissButton);

      expect(screen.queryByText(/published successfully/i)).not.toBeInTheDocument();
    });
  });

  describe('SEO field character limits', () => {
    it('shows a live counter for the meta description (textarea, no raw maxLength)', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      // 'SEO Description' (from mock) is 15 characters
      const metaDescription = screen.getByDisplayValue('SEO Description');
      // A multi-line textarea, not a single-line input, so long copy is editable.
      expect(metaDescription.tagName).toBe('TEXTAREA');
      // No browser maxLength: it caps the RAW length while the counter/server use
      // the TRIMMED length, so a maxLength would silently block typing on a
      // whitespace-padded value. The counter is the single source of truth.
      expect(metaDescription).not.toHaveAttribute('maxLength');
      expect(screen.getByText('15/160 characters')).toBeInTheDocument();
      // Under the limit the separate live region is empty — no screen-reader
      // announcement while typing normally (only the over-limit state announces).
      expect(screen.queryByText(/character limit exceeded/i)).not.toBeInTheDocument();
    });

    it('shows a live counter for the meta title (no raw maxLength)', async () => {
      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      // 'SEO Title' (from mock) is 9 characters
      const metaTitle = screen.getByDisplayValue('SEO Title');
      expect(metaTitle).not.toHaveAttribute('maxLength');
      expect(screen.getByText('9/60 characters')).toBeInTheDocument();
    });

    it('flags a pre-existing over-limit meta description so the dealer can shorten it', async () => {
      const longDescription = 'x'.repeat(187);
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                page: { ...mockPageData.page, metaDescription: longDescription },
              }),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      expect(screen.getByText(/187\/160 characters/)).toBeInTheDocument();
      expect(screen.getByText(/please shorten to save/i)).toBeInTheDocument();
      // Over-limit is the one actionable state, so the separate polite live
      // region announces it (the visible counter itself is not a live region).
      expect(screen.getByText(/character limit exceeded/i)).toHaveAttribute('aria-live', 'polite');
    });

    it('auto-opens the Settings panel when a save is blocked by field validation', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: false,
              json: () =>
                Promise.resolve({
                  fieldErrors: {
                    metaDescription: ['Meta description should be 160 characters or less'],
                  },
                }),
            });
          }
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPageData) });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      // Settings panel starts collapsed (its Title field is not rendered)
      expect(screen.queryByDisplayValue('Test Page')).not.toBeInTheDocument();

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      // After the validation failure the panel opens so the dealer can see and
      // fix the offending field, and the specific error is surfaced.
      await waitFor(() => {
        expect(
          screen.getByText(/meta description should be 160 characters or less/i)
        ).toBeInTheDocument();
      });
      expect(screen.getByDisplayValue('Test Page')).toBeInTheDocument();
      // The editor must stay mounted — a save error must NOT tear it down and
      // discard the dealer's unsaved Puck edits (the load-bearing guard fix).
      expect(screen.getByTestId('puck-editor')).toBeInTheDocument();
    });

    it('does not flag a value that is over the raw length but within limit once trimmed', async () => {
      // 159 visible chars + 2 trailing spaces = 161 raw, 159 trimmed. The save
      // sends the trimmed value (159) which the server accepts, so the counter
      // must reflect 159/160 and NOT show the over-limit warning.
      const trailingSpaceValue = 'x'.repeat(159) + '  ';
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                page: { ...mockPageData.page, metaDescription: trailingSpaceValue },
              }),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      expect(screen.getByText('159/160 characters')).toBeInTheDocument();
      expect(screen.queryByText(/please shorten to save/i)).not.toBeInTheDocument();
    });

    it('auto-opens Settings when the Puck save (onPublish) path hits field validation', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: false,
              json: () =>
                Promise.resolve({
                  fieldErrors: {
                    metaDescription: ['Meta description should be 160 characters or less'],
                  },
                }),
            });
          }
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPageData) });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);

      await waitFor(() => {
        expect(screen.getByTestId('puck-save-btn')).toBeInTheDocument();
      });

      // Settings starts collapsed
      expect(screen.queryByDisplayValue('Test Page')).not.toBeInTheDocument();

      await userEvent.click(screen.getByTestId('puck-save-btn'));

      await waitFor(() => {
        expect(screen.getByDisplayValue('Test Page')).toBeInTheDocument();
      });
      expect(screen.getByTestId('puck-editor')).toBeInTheDocument();
    });

    it('treats a value at exactly the limit (160) as valid — no warning', async () => {
      // Guards the `count > max` boundary against an off-by-one flip to `>=`:
      // 160 chars is within the 160 limit and must NOT warn.
      const atLimit = 'x'.repeat(160);
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({ page: { ...mockPageData.page, metaDescription: atLimit } }),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);
      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });
      await userEvent.click(screen.getByRole('button', { name: /settings/i }));
      expect(screen.getByText('160/160 characters')).toBeInTheDocument();
      expect(screen.queryByText(/please shorten to save/i)).not.toBeInTheDocument();
    });

    it('flags a value one character over the limit (161)', async () => {
      // The matching upper boundary: 161 must warn. Provided as a pre-existing
      // loaded value (the migrated-page case the fix targets).
      const overLimit = 'x'.repeat(161);
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({ page: { ...mockPageData.page, metaDescription: overLimit } }),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);
      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });
      await userEvent.click(screen.getByRole('button', { name: /settings/i }));
      // Over-limit text is "161/160 characters — please shorten to save", so
      // match a substring (the at-limit case below can use an exact string).
      expect(screen.getByText(/161\/160 characters/)).toBeInTheDocument();
      expect(screen.getByText(/please shorten to save/i)).toBeInTheDocument();
    });

    it('clears the inline validation banner once the dealer fixes the field and saves again', async () => {
      // The core fix-and-retry journey: a save blocked by field validation shows
      // the banner inline (editor stays mounted); the next successful save must
      // clear it. Guards the setError(null) reset at the top of the save path.
      let putCalls = 0;
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            putCalls += 1;
            if (putCalls === 1) {
              return Promise.resolve({
                ok: false,
                json: () =>
                  Promise.resolve({
                    fieldErrors: {
                      metaDescription: ['Meta description should be 160 characters or less'],
                    },
                  }),
              });
            }
            return Promise.resolve({
              ok: true,
              json: () => Promise.resolve({ page: mockPageData.page }),
            });
          }
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPageData) });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);
      await waitFor(() => {
        expect(screen.getByTestId('puck-save-btn')).toBeInTheDocument();
      });

      // First save fails with the validation error.
      await userEvent.click(screen.getByTestId('puck-save-btn'));
      await waitFor(() => {
        expect(
          screen.getByText(/meta description should be 160 characters or less/i)
        ).toBeInTheDocument();
      });

      // Second save succeeds — the stale error banner must disappear.
      await userEvent.click(screen.getByTestId('puck-save-btn'));
      await waitFor(() => {
        expect(screen.getByText(/page saved successfully/i)).toBeInTheDocument();
      });
      expect(
        screen.queryByText(/meta description should be 160 characters or less/i)
      ).not.toBeInTheDocument();
    });

    it('keeps the editor mounted on a generic (non-field) save error', async () => {
      // The load-bearing guard change (`if (error || !page)` → `if (!page)`):
      // even a generic publish error must NOT tear down the Puck editor and
      // discard unsaved edits. The old guard would render the full-screen error
      // view (no puck-editor) here.
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: false,
              json: () => Promise.resolve({ error: 'Something went wrong' }),
            });
          }
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPageData) });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);
      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
      });
      // Editor survives — old guard would have unmounted it into the error view.
      expect(screen.getByTestId('puck-editor')).toBeInTheDocument();
      expect(screen.queryByRole('link', { name: /back to pages/i })).not.toBeInTheDocument();
    });

    it('shows excerpt and author counters on blog pages', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string) => {
        if (url === '/api/cms/pages/page-123') {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                page: { ...mockPageData.page, type: 'blog', excerpt: 'Hello', author: 'Jane' },
              }),
          });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);
      await waitFor(() => {
        expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument();
      });
      await userEvent.click(screen.getByRole('button', { name: /settings/i }));

      // 'Hello' = 5 chars / 'Jane' = 4 chars; both blog-only fields now carry the
      // same friendly counter as the SEO fields.
      expect(screen.getByText('5/500 characters')).toBeInTheDocument();
      expect(screen.getByText('4/100 characters')).toBeInTheDocument();
    });

    it('does not auto-open Settings when the only field error is a non-panel field', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({
              ok: false,
              json: () =>
                Promise.resolve({ fieldErrors: { featuredImage: ['Must be a valid URL'] } }),
            });
          }
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPageData) });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);
      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByText(/must be a valid url/i)).toBeInTheDocument();
      });
      // featuredImage isn't in the Settings panel, so it stays collapsed rather
      // than popping open with nothing visibly wrong.
      expect(screen.queryByDisplayValue('Test Page')).not.toBeInTheDocument();
    });

    it('surfaces a generic message instead of a silent empty banner for empty fieldErrors', async () => {
      (global.fetch as jest.Mock).mockImplementation((url: string, options?: RequestInit) => {
        if (url === '/api/cms/pages/page-123') {
          if (options?.method === 'PUT') {
            return Promise.resolve({ ok: false, json: () => Promise.resolve({ fieldErrors: {} }) });
          }
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPageData) });
        }
        if (url === '/api/dealer') {
          return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDealerData) });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

      render(<PageEditorPage />);
      await waitFor(() => {
        expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
      });

      await userEvent.click(screen.getByRole('button', { name: /publish/i }));

      await waitFor(() => {
        expect(screen.getByText(/validation failed/i)).toBeInTheDocument();
      });
    });
  });
});
