/**
 * Tests for FloatingPublishButton Component (Simplified)
 *
 * Component is now only rendered on /dashboard (controlled by parent layout).
 * No EditorContext integration or pathname detection.
 */

import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { FloatingPublishButton, DealerPublishInfo } from '../FloatingPublishButton';
import { ZO_REQUIRED_MESSAGE } from '@/lib/zo-messages';

// Mock fetch globally
global.fetch = jest.fn();

describe('FloatingPublishButton', () => {
  const mockActiveDealer: DealerPublishInfo = {
    id: 'dealer123',
    subdomain: 'bobsoil',
    domain: 'com',
    domainPrefix: 'myamsoil',
    status: 'active',
    subscriptionTier: 'growth',
    updatedAt: new Date('2025-11-20T10:00:00Z'),
    lastPublishedAt: new Date('2025-11-19T10:00:00Z'), // 1 day old
  };

  const mockPendingDealer: DealerPublishInfo = {
    ...mockActiveDealer,
    status: 'pending',
    lastPublishedAt: null,
  };

  beforeEach(() => {
    jest.clearAllMocks();
    jest.useFakeTimers();
    (global.fetch as jest.Mock).mockClear();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  describe('Visibility Logic', () => {
    it('should render when unpublished changes exist (updatedAt > lastPublishedAt)', () => {
      render(<FloatingPublishButton dealer={mockActiveDealer} />);
      expect(screen.getByText(/Publish Changes/i)).toBeInTheDocument();
    });

    it('should not render when no unpublished changes (updatedAt <= lastPublishedAt)', () => {
      const dealerNoChanges: DealerPublishInfo = {
        ...mockActiveDealer,
        updatedAt: new Date('2025-11-18T10:00:00Z'), // Older than lastPublishedAt
      };

      const { container } = render(<FloatingPublishButton dealer={dealerNoChanges} />);
      expect(container.firstChild).toBeNull();
    });

    it('should not render when updatedAt equals lastPublishedAt', () => {
      const dealerSameTime: DealerPublishInfo = {
        ...mockActiveDealer,
        updatedAt: new Date('2025-11-19T10:00:00Z'),
        lastPublishedAt: new Date('2025-11-19T10:00:00Z'),
      };

      const { container } = render(<FloatingPublishButton dealer={dealerSameTime} />);
      expect(container.firstChild).toBeNull();
    });

    it('should render when lastPublishedAt is null (never published)', () => {
      const dealerNeverPublished: DealerPublishInfo = {
        ...mockActiveDealer,
        lastPublishedAt: null,
      };

      render(<FloatingPublishButton dealer={dealerNeverPublished} />);
      expect(screen.getByText(/Publish Changes/i)).toBeInTheDocument();
    });

    it('should show "Publish Site" for first-time publish (pending status)', () => {
      render(<FloatingPublishButton dealer={mockPendingDealer} />);
      expect(screen.getByText(/Publish Site/i)).toBeInTheDocument();
    });

    it('should show "Publish Changes" for active dealers with changes', () => {
      render(<FloatingPublishButton dealer={mockActiveDealer} />);
      expect(screen.getByText(/Publish Changes/i)).toBeInTheDocument();
    });
  });

  describe('Publishing Flow', () => {
    it('should show "Publishing..." state when clicked', async () => {
      let resolvePromise: (value: unknown) => void;
      const fetchPromise = new Promise((resolve) => {
        resolvePromise = resolve;
      });

      (global.fetch as jest.Mock).mockImplementationOnce(() => fetchPromise);

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      await act(async () => {
        fireEvent.click(button);
      });

      expect(screen.getByText(/Publishing/i)).toBeInTheDocument();

      await act(async () => {
        resolvePromise!({
          ok: true,
          json: async () => ({ success: true }),
        });
      });

      await act(async () => {
        jest.runAllTimers();
      });
    });

    it('should call publish API endpoint', async () => {
      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      await act(async () => {
        fireEvent.click(button);
      });

      await waitFor(() => {
        expect(global.fetch).toHaveBeenCalledWith(
          '/api/dealers/dealer123/publish',
          expect.objectContaining({
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({}),
          })
        );
      });

      await act(async () => {
        jest.runAllTimers();
      });
    });

    it('should show "Published!" state on success', async () => {
      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      await act(async () => {
        fireEvent.click(button);
      });

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

      await act(async () => {
        jest.runAllTimers();
      });
    });

    it('should return to ready state after success timeout (2 seconds)', async () => {
      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      await act(async () => {
        fireEvent.click(button);
      });

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

      await act(async () => {
        jest.advanceTimersByTime(2000);
      });

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

    it('should only call API once when clicked multiple times rapidly', async () => {
      jest.useRealTimers();

      (global.fetch as jest.Mock).mockResolvedValue({
        ok: true,
        json: async () => ({ success: true }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);

      fireEvent.click(button);
      fireEvent.click(button);
      fireEvent.click(button);

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

      expect(global.fetch).toHaveBeenCalledTimes(1);
    });
  });

  describe('Error Handling', () => {
    it('surfaces the actionable ZO message on a 422 (#852, end-to-end)', async () => {
      jest.useRealTimers();

      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: false,
        status: 422,
        json: async () => ({ error: ZO_REQUIRED_MESSAGE, code: 'ZO_REQUIRED' }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);
      fireEvent.click(screen.getByText(/Publish Changes/i));

      // handlePublish must parse body.error and show the ZO message verbatim,
      // not a generic "Update failed".
      await waitFor(() => {
        expect(screen.getByText(ZO_REQUIRED_MESSAGE)).toBeInTheDocument();
      });
    });

    it('should show error message when API returns error', async () => {
      jest.useRealTimers();

      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: false,
        status: 500,
        json: async () => ({ error: 'Failed to publish site' }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      fireEvent.click(button);

      // The specific server message is surfaced to the dealer (#852), not a
      // generic string — that is the whole point of the change.
      await waitFor(() => {
        expect(screen.getByText('Failed to publish site')).toBeInTheDocument();
      });
    });

    it('should show Retry button on error', async () => {
      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: false,
        status: 500,
        json: async () => ({ error: 'Failed' }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      await act(async () => {
        fireEvent.click(button);
      });

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

    it('should retry publish when Retry button is clicked', async () => {
      jest.useRealTimers();

      (global.fetch as jest.Mock)
        .mockResolvedValueOnce({
          ok: false,
          status: 500,
          json: async () => ({ error: 'Failed' }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ success: true }),
        });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      fireEvent.click(button);

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

      const retryButton = screen.getByText(/Retry/i);
      fireEvent.click(retryButton);

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

      expect(global.fetch).toHaveBeenCalledTimes(2);
    });

    it('should handle network errors gracefully', async () => {
      jest.useRealTimers();

      (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      fireEvent.click(button);

      // Thrown Error.message is surfaced (#852).
      await waitFor(() => {
        expect(screen.getByText('Network error')).toBeInTheDocument();
      });
    });

    it('should hide View Live Site link when in error state', async () => {
      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: false,
        status: 500,
        json: async () => ({ error: 'Failed' }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      expect(screen.getByText(/View Live Site/i)).toBeInTheDocument();

      const button = screen.getByText(/Publish Changes/i);
      await act(async () => {
        fireEvent.click(button);
      });

      await waitFor(() => {
        // 'Failed' is the mocked response body's `error` (line ~337): the
        // route returns { error: 'Failed' } → handlePublish throws
        // new Error('Failed') → errorMessage === 'Failed' is rendered.
        expect(screen.getByText('Failed')).toBeInTheDocument();
        expect(screen.queryByText(/View Live Site/i)).not.toBeInTheDocument();
      });
    });
  });

  describe('View Live Site Link', () => {
    it('should show "View Live Site" link when dealer is active', () => {
      render(<FloatingPublishButton dealer={mockActiveDealer} />);
      expect(screen.getByText(/View Live Site/i)).toBeInTheDocument();
    });

    it('should not show "View Live Site" when dealer is pending', () => {
      render(<FloatingPublishButton dealer={mockPendingDealer} />);
      expect(screen.queryByText(/View Live Site/i)).not.toBeInTheDocument();
    });

    it('should show "Preview Site" link for first-time publish', () => {
      render(<FloatingPublishButton dealer={mockPendingDealer} />);
      expect(screen.getByText(/Preview Site/i)).toBeInTheDocument();
    });

    it('should open View Live Site link in new tab', () => {
      render(<FloatingPublishButton dealer={mockActiveDealer} />);
      const link = screen.getByText(/View Live Site/i).closest('a');
      expect(link).toHaveAttribute('target', '_blank');
      expect(link).toHaveAttribute('rel', 'noopener noreferrer');
    });

    it('should use domainPrefix in URL (myamsoil)', () => {
      // Jest runs with NODE_ENV='test', so production URL format is used
      // URL format: https://{subdomain}.{domainPrefix}.{domain}
      render(<FloatingPublishButton dealer={mockActiveDealer} />);
      const link = screen.getByText(/View Live Site/i).closest('a');
      expect(link).toHaveAttribute('href', 'https://bobsoil.myamsoil.com');
    });

    it('should use domainPrefix in URL (shopamsoil)', () => {
      const shopamsoilDealer: DealerPublishInfo = {
        ...mockActiveDealer,
        domainPrefix: 'shopamsoil',
      };
      render(<FloatingPublishButton dealer={shopamsoilDealer} />);
      const link = screen.getByText(/View Live Site/i).closest('a');
      // With shopamsoil prefix, URL should use shopamsoil instead of myamsoil
      expect(link).toHaveAttribute('href', 'https://bobsoil.shopamsoil.com');
    });

    describe('Development URL format (new [subdomain]/[domainSlug] route)', () => {
      const ORIGINAL_NODE_ENV = process.env.NODE_ENV;

      beforeEach(() => {
        // Simulate NODE_ENV=development so the dev-path branch is taken
        Object.defineProperty(process.env, 'NODE_ENV', {
          value: 'development',
          configurable: true,
        });
      });

      afterEach(() => {
        Object.defineProperty(process.env, 'NODE_ENV', {
          value: ORIGINAL_NODE_ENV,
          configurable: true,
        });
      });

      it('builds the dev URL with the domainSlug segment for myamsoil dealers', () => {
        render(<FloatingPublishButton dealer={mockActiveDealer} />);
        const link = screen.getByText(/View Live Site/i).closest('a');
        // Route is /dealers/[subdomain]/[domainSlug]; the single-segment
        // form /dealers/bobsoil 404s, so both segments are required.
        expect(link).toHaveAttribute('href', '/dealers/bobsoil/myamsoil-com');
      });

      it('builds the dev URL with the correct domainSlug for shopamsoil dealers', () => {
        const shopamsoilDealer: DealerPublishInfo = {
          ...mockActiveDealer,
          domainPrefix: 'shopamsoil',
        };
        render(<FloatingPublishButton dealer={shopamsoilDealer} />);
        const link = screen.getByText(/View Live Site/i).closest('a');
        expect(link).toHaveAttribute('href', '/dealers/bobsoil/shopamsoil-com');
      });

      it('builds the dev URL with .ca suffix for Canadian dealers', () => {
        const canadianDealer: DealerPublishInfo = {
          ...mockActiveDealer,
          domain: 'ca',
        };
        render(<FloatingPublishButton dealer={canadianDealer} />);
        const link = screen.getByText(/View Live Site/i).closest('a');
        expect(link).toHaveAttribute('href', '/dealers/bobsoil/myamsoil-ca');
      });
    });
  });

  describe('Accessibility', () => {
    it('should have accessible publish button', () => {
      render(<FloatingPublishButton dealer={mockActiveDealer} />);
      const button = screen.getByText(/Publish Changes/i);
      expect(button.tagName).toBe('BUTTON');
    });

    it('should have accessible retry button', async () => {
      jest.useRealTimers();

      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: false,
        status: 500,
        json: async () => ({ error: 'Failed' }),
      });

      render(<FloatingPublishButton dealer={mockActiveDealer} />);

      const button = screen.getByText(/Publish Changes/i);
      fireEvent.click(button);

      await waitFor(() => {
        const retryButton = screen.getByText(/Retry/i);
        expect(retryButton.tagName).toBe('BUTTON');
      });
    });
  });
});
