import { renderHook, act } from '@testing-library/react';
import axios from 'axios';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { useAutoSave } from './useAutoSave';

// Mock axios
vi.mock('axios');
const mockedAxios = vi.mocked(axios);

// Mock route helper
global.route = vi.fn((name, params) => {
  if (name === 'content-editor.update') {
    return `/sites/${params.site}/drafts/${params.aiDraft}/content`;
  }
  return '/mock-route';
});

describe('useAutoSave', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();
    mockedAxios.patch = vi.fn().mockResolvedValue({ data: {} });
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.useRealTimers();
  });

  it('should initialize with correct default state', () => {
    const { result } = renderHook(() =>
      useAutoSave({
        content: 'initial content',
        siteId: 1,
        draftId: 1,
      }),
    );

    expect(result.current.isSaving).toBe(false);
    expect(result.current.hasUnsavedChanges).toBe(false);
    expect(result.current.lastSavedAt).toBe(null);
    expect(result.current.error).toBe(null);
    expect(result.current.saveNow).toBeInstanceOf(Function);
  });

  it('should track unsaved changes when content changes', () => {
    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    expect(result.current.hasUnsavedChanges).toBe(false);

    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);
  });

  it('should auto-save after interval when there are unsaved changes', async () => {
    const { rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
          intervalMs: 30000,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content to trigger unsaved changes
    act(() => {
      rerender({ content: 'modified content' });
    });

    // Fast-forward time by 30 seconds
    await act(async () => {
      vi.advanceTimersByTime(30000);
      await Promise.resolve(); // Flush promises
    });

    expect(mockedAxios.patch).toHaveBeenCalledWith(
      '/sites/1/drafts/1/content',
      { content: 'modified content' },
      expect.objectContaining({
        signal: expect.any(AbortSignal),
      }),
    );
  });

  it('should not auto-save if there are no unsaved changes', async () => {
    renderHook(() =>
      useAutoSave({
        content: 'initial content',
        siteId: 1,
        draftId: 1,
        intervalMs: 30000,
      }),
    );

    // Fast-forward time by 30 seconds
    await act(async () => {
      vi.advanceTimersByTime(30000);
      await Promise.resolve();
    });

    expect(mockedAxios.patch).not.toHaveBeenCalled();
  });

  it('should not auto-save when disabled', async () => {
    const { rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
          enabled: false,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    // Fast-forward time
    await act(async () => {
      vi.advanceTimersByTime(30000);
      await Promise.resolve();
    });

    expect(mockedAxios.patch).not.toHaveBeenCalled();
  });

  it('should save immediately when saveNow is called', async () => {
    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content to create unsaved changes
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Call saveNow
    await act(async () => {
      await result.current.saveNow();
    });

    expect(mockedAxios.patch).toHaveBeenCalledWith(
      '/sites/1/drafts/1/content',
      { content: 'modified content' },
      expect.objectContaining({
        signal: expect.any(AbortSignal),
      }),
    );
  });

  it('should set isSaving state during save operation', async () => {
    let resolveAxios: (value: unknown) => void;
    const axiosPromise = new Promise((resolve) => {
      resolveAxios = resolve;
    });
    mockedAxios.patch = vi.fn().mockReturnValue(axiosPromise);

    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Start save
    let savePromise: Promise<void>;
    act(() => {
      savePromise = result.current.saveNow();
    });

    expect(result.current.isSaving).toBe(true);

    // Complete save
    await act(async () => {
      resolveAxios!({ data: {} });
      await savePromise!;
    });

    expect(result.current.isSaving).toBe(false);
  });

  it('should clear unsaved changes after successful save', async () => {
    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Save
    await act(async () => {
      await result.current.saveNow();
    });

    expect(result.current.hasUnsavedChanges).toBe(false);
  });

  it('should update lastSavedAt after successful save', async () => {
    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    expect(result.current.lastSavedAt).toBe(null);

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Save
    await act(async () => {
      await result.current.saveNow();
    });

    expect(result.current.lastSavedAt).toBeInstanceOf(Date);
  });

  it('should handle save errors', async () => {
    const errorMessage = 'Save failed';
    mockedAxios.patch = vi.fn().mockRejectedValue({
      response: { data: { message: errorMessage } },
      isAxiosError: true,
    });
    mockedAxios.isAxiosError = vi
      .fn()
      .mockReturnValue(true) as unknown as typeof mockedAxios.isAxiosError;

    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Save
    await act(async () => {
      await result.current.saveNow();
    });

    expect(result.current.error).toBe(errorMessage);
    expect(result.current.isSaving).toBe(false);
  });

  it('should handle network errors without response', async () => {
    mockedAxios.patch = vi.fn().mockRejectedValue(new Error('Network error'));
    mockedAxios.isAxiosError = vi
      .fn()
      .mockReturnValue(false) as unknown as typeof mockedAxios.isAxiosError;

    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Save
    await act(async () => {
      await result.current.saveNow();
    });

    expect(result.current.error).toBe('Failed to save content');
    expect(result.current.isSaving).toBe(false);
  });

  it('should not save if already saving', async () => {
    let resolveAxios: (value: unknown) => void;
    const axiosPromise = new Promise((resolve) => {
      resolveAxios = resolve;
    });
    mockedAxios.patch = vi.fn().mockReturnValue(axiosPromise);

    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Start first save
    let firstSave: Promise<void>;
    act(() => {
      firstSave = result.current.saveNow();
    });

    expect(result.current.isSaving).toBe(true);

    // Try to save again
    await act(async () => {
      await result.current.saveNow();
    });

    // Should only have been called once
    expect(mockedAxios.patch).toHaveBeenCalledTimes(1);

    // Complete save
    await act(async () => {
      resolveAxios!({ data: {} });
      await firstSave!;
    });
  });

  it('should abort pending requests on unmount', async () => {
    mockedAxios.patch = vi.fn().mockImplementation(() => {
      return new Promise(() => {}); // Never resolves
    });

    const { result, rerender, unmount } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Start save
    act(() => {
      result.current.saveNow();
    });

    expect(result.current.isSaving).toBe(true);

    // Unmount before save completes
    unmount();

    // Verify the request was made with an AbortSignal
    expect(mockedAxios.patch).toHaveBeenCalledWith(
      expect.any(String),
      expect.any(Object),
      expect.objectContaining({
        signal: expect.any(AbortSignal),
      }),
    );
  });

  it('should use custom interval when provided', async () => {
    const customInterval = 10000; // 10 seconds

    const { rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
          intervalMs: customInterval,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    // Fast-forward by custom interval
    await act(async () => {
      vi.advanceTimersByTime(customInterval);
      await Promise.resolve();
    });

    expect(mockedAxios.patch).toHaveBeenCalled();
  });

  it('should clear error on successful save after error', async () => {
    // First call fails
    mockedAxios.patch = vi.fn().mockRejectedValueOnce({
      response: { data: { message: 'Error' } },
      isAxiosError: true,
    });
    mockedAxios.isAxiosError = vi
      .fn()
      .mockReturnValue(true) as unknown as typeof mockedAxios.isAxiosError;

    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // First save fails
    await act(async () => {
      await result.current.saveNow();
    });

    expect(result.current.error).toBe('Error');

    // Second call succeeds
    mockedAxios.patch = vi.fn().mockResolvedValue({ data: {} });

    // Change content again
    act(() => {
      rerender({ content: 'new modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Second save succeeds
    await act(async () => {
      await result.current.saveNow();
    });

    expect(result.current.error).toBe(null);
  });

  it('should track unsaved changes correctly across saves', async () => {
    const { result, rerender } = renderHook(
      ({ content }) =>
        useAutoSave({
          content,
          siteId: 1,
          draftId: 1,
        }),
      { initialProps: { content: 'initial content' } },
    );

    // Change content
    act(() => {
      rerender({ content: 'modified content' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);

    // Save
    await act(async () => {
      await result.current.saveNow();
    });

    expect(result.current.hasUnsavedChanges).toBe(false);

    // Change back to different content
    act(() => {
      rerender({ content: 'another change' });
    });

    expect(result.current.hasUnsavedChanges).toBe(true);
  });
});
