import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../test/utils';

vi.mock('../lib/api', () => ({
  api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
  ApiError: class extends Error {},
}));
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock('../hooks/usePermissions', () => ({ usePermissions: () => ({ can: () => true }) }));

import { Routes, Route } from 'react-router-dom';
import { api } from '../lib/api';
import ResourceDetailPage from './ResourceDetailPage';

const get = api.get as ReturnType<typeof vi.fn>;
const patch = api.patch as ReturnType<typeof vi.fn>;

const resource = {
  uuid: 'r1',
  name: 'لیزر دایود',
  address_uuid: 'a1',
  address_name: 'شعبهٔ مرکزی',
  type_uuid: 't1',
  type_code: 'device',
  type_name: 'دستگاه لیزر',
  capacity: 1,
  setup_minutes: 5,
  cleanup_minutes: 10,
  attributes: {},
  subject_kind: null,
  subject_uuid: null,
  skills: [{ skill_uuid: 's1', skill_name: 'کار با لیزر', level: 4 }],
  categories: [{ uuid: 'c-hand', name: 'دست' }],
  active: true,
  created_at: 0,
  updated_at: 0,
  upcoming_appointments: 0,
};

const emptyDays = (): Record<string, unknown[]> =>
  Object.fromEntries(Array.from({ length: 7 }, (_, d) => [String(d), [] as unknown[]]));

function mockApi(days: Record<string, unknown[]> = emptyDays()) {
  get.mockImplementation((path: string) => {
    if (path === '/api/v1/resource/r1') return Promise.resolve({ success: true, data: resource });
    if (path.endsWith('/calendar')) {
      return Promise.resolve({
        success: true,
        data: { resource_uuid: 'r1', timezone: 'Asia/Tehran', defined: true, days },
      });
    }
    if (path.includes('/service-categories/tree')) {
      return Promise.resolve({
        success: true,
        data: [{ uuid: 'c-hand', name: 'دست', sort_order: 0, active: true, children: [] },
               { uuid: 'c-foot', name: 'پا', sort_order: 1, active: true, children: [] }],
      });
    }
    return Promise.resolve({ success: true, data: [] });
  });
}

function renderPage(route = '/admin/resources/r1') {
  return renderWithProviders(
    <Routes>
      <Route path="/admin/resources/:resourceUuid" element={<ResourceDetailPage />} />
    </Routes>,
    { route },
  );
}

describe('ResourceDetailPage', () => {
  beforeEach(() => vi.clearAllMocks());

  it('اطلاعات منبع را در تب پیش‌فرض نشان می‌دهد', async () => {
    mockApi();
    renderPage();

    await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
    expect(screen.getByText('ظرفیت هم‌زمان')).toBeInTheDocument();
    expect(screen.getByText('کار با لیزر · 4')).toBeInTheDocument();
  });

  it('تب از URL خوانده می‌شود تا بازگشت و رفرش همان نما را بدهد', async () => {
    mockApi();
    renderPage('/admin/resources/r1?tab=hours');

    await waitFor(() => expect(screen.getByText('شنبه')).toBeInTheDocument());
    expect(screen.getByText('جمعه')).toBeInTheDocument();
    expect(screen.getAllByText('بدون شیفت — این روز بسته است')).toHaveLength(7);
  });

  it('شیفت ذخیره‌شده را به‌صورت ساعت نشان می‌دهد', async () => {
    const days = emptyDays();
    days['0'] = [{ sequence: 0, start_minute: 540, end_minute: 1020, start_time: '09:00', end_time: '17:00', active: true }];
    mockApi(days);
    renderPage('/admin/resources/r1?tab=hours');

    await waitFor(() => expect(screen.getByDisplayValue('09:00')).toBeInTheDocument());
    expect(screen.getByDisplayValue('17:00')).toBeInTheDocument();
  });

  /** نوار ذخیره فقط وقتی ظاهر می‌شود که چیزی تغییر کرده باشد — خودش نشانهٔ dirty است. */
  it('نوار ذخیره تا وقتی تغییری نباشد نمی‌آید', async () => {
    const user = userEvent.setup();
    mockApi();
    renderPage('/admin/resources/r1?tab=hours');

    await waitFor(() => expect(screen.getByText('شنبه')).toBeInTheDocument());
    expect(screen.queryByRole('button', { name: 'ذخیرهٔ شیفت‌ها' })).not.toBeInTheDocument();

    await user.click(screen.getAllByRole('button', { name: 'شیفت' })[0]);

    expect(await screen.findByText('تغییرات شیفت ذخیره نشده است')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'ذخیرهٔ شیفت‌ها' })).toBeInTheDocument();
  });

  /** «بازگرداندن» باید به نسخهٔ سرور برگردد، نه صرفاً آخرین ویرایش را لغو کند. */
  it('بازگرداندن، draft را به نسخهٔ سرور برمی‌گرداند', async () => {
    const user = userEvent.setup();
    mockApi();
    renderPage('/admin/resources/r1?tab=hours');

    await waitFor(() => expect(screen.getByText('شنبه')).toBeInTheDocument());
    await user.click(screen.getAllByRole('button', { name: 'شیفت' })[0]);
    await user.click(await screen.findByRole('button', { name: 'بازگرداندن' }));

    await waitFor(() =>
      expect(screen.queryByText('تغییرات شیفت ذخیره نشده است')).not.toBeInTheDocument());
    expect(screen.getAllByText('بدون شیفت — این روز بسته است')).toHaveLength(7);
  });

  /**
   * دو شیفتِ متداخل در یک روز نباید ذخیره شود. سرور هم همین را رد می‌کند، ولی خطا
   * باید حین ویرایش دیده شود نه بعد از رفت‌وبرگشت شبکه.
   */
  it('شیفت‌های متداخل را رد می‌کند و ذخیره را می‌بندد', async () => {
    const days = emptyDays();
    days['0'] = [
      { sequence: 0, start_minute: 540,  end_minute: 780,  start_time: '09:00', end_time: '13:00', active: true },
      { sequence: 1, start_minute: 1020, end_minute: 1140, start_time: '17:00', end_time: '19:00', active: true },
    ];
    const user = userEvent.setup();
    mockApi(days);
    renderPage('/admin/resources/r1?tab=hours');

    await waitFor(() => expect(screen.getByDisplayValue('17:00')).toBeInTheDocument());
    expect(screen.queryByText(/تداخل دارند/)).not.toBeInTheDocument();

    // ۱۷:۰۰–۱۹:۰۰ به ۱۲:۰۰ کشیده می‌شود و با ۰۹:۰۰–۱۳:۰۰ تداخل پیدا می‌کند.
    await user.clear(screen.getByLabelText('ساعت شروع شیفت ۲ روز شنبه'));
    await user.type(screen.getByLabelText('ساعت شروع شیفت ۲ روز شنبه'), '12:00');

    // پیام دو جا می‌آید و باید بیاید: کنار خودِ روز، و در نوار ذخیره که دلیل قفل را می‌گوید.
    expect(await screen.findAllByText(/شیفت‌های روز شنبه با هم تداخل دارند/)).toHaveLength(2);
    expect(screen.getByRole('button', { name: 'ذخیرهٔ شیفت‌ها' })).toBeDisabled();
  });

  /** بازهٔ بدون تداخل نباید قربانی اعتبارسنجی شود — مرز چسبیده مجاز است. */
  it('شیفت‌های چسبیده اما بدون هم‌پوشانی را می‌پذیرد', async () => {
    const days = emptyDays();
    days['0'] = [
      { sequence: 0, start_minute: 540, end_minute: 780,  start_time: '09:00', end_time: '13:00', active: true },
      { sequence: 1, start_minute: 780, end_minute: 1020, start_time: '13:00', end_time: '17:00', active: true },
    ];
    const user = userEvent.setup();
    mockApi(days);
    renderPage('/admin/resources/r1?tab=hours');

    await waitFor(() => expect(screen.getByDisplayValue('09:00')).toBeInTheDocument());
    expect(screen.queryByText(/تداخل دارند/)).not.toBeInTheDocument();

    await user.click(screen.getAllByRole('button', { name: 'شیفت' })[1]);

    expect(await screen.findByRole('button', { name: 'ذخیرهٔ شیفت‌ها' })).toBeEnabled();
  });

  /** آیکون کپی گمراه‌کننده بود و حذف شد. */
  it('دکمهٔ کپی شیفت ندارد', async () => {
    const days = emptyDays();
    days['0'] = [{ sequence: 0, start_minute: 540, end_minute: 1020, start_time: '09:00', end_time: '17:00', active: true }];
    mockApi(days);
    renderPage('/admin/resources/r1?tab=hours');

    await waitFor(() => expect(screen.getByDisplayValue('09:00')).toBeInTheDocument());
    expect(screen.queryByRole('button', { name: /کپی/ })).not.toBeInTheDocument();
  });

  it('تب دسته‌بندی فقط انتخاب می‌دهد، نه ساخت', async () => {
    mockApi();
    const user = userEvent.setup();
    renderPage();

    await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
    await user.click(screen.getByRole('button', { name: 'دسته‌بندی‌ها' }));

    await waitFor(() => expect(screen.getByText(/فقط انتخاب می‌شود/)).toBeInTheDocument());
    expect(screen.queryByRole('button', { name: /افزودن دسته‌بندی جدید/ })).not.toBeInTheDocument();
    expect(screen.getByText('تنظیمات ← دسته‌بندی‌ها')).toHaveAttribute('href', '/admin/service-categories');
  });

  /** تب استثناها فقط دو ورودی دارد: تعطیلات رسمی، و مرخصی و سرویس. */
  it('تب تعطیلات پیش‌نمایش دو هفته را ندارد', async () => {
    mockApi();
    renderPage('/admin/resources/r1?tab=exceptions');

    await waitFor(() => expect(screen.getByText('مرخصی و سرویس')).toBeInTheDocument());
    expect(screen.queryByText('پیش‌نمایش دو هفته')).not.toBeInTheDocument();
    expect(screen.queryByText(/نوبت‌های ثبت‌شده هنوز از آن کسر نشده‌اند/)).not.toBeInTheDocument();
  });

  /**
   * مسدودسازی موردی از مودالِ جدول به تبِ خودِ منبع آمد و «غیرفعال‌سازی موقت» نام گرفت.
   * متن تب باید تفاوتش با استثنای تقویم را بگوید، وگرنه اپراتور تعطیلیِ یک بعدازظهر را
   * برای همیشه ثبت می‌کند.
   */
  it('تب غیرفعال‌سازی موقت، بازه‌ها را جدا از الگوی کاری معرفی می‌کند', async () => {
    mockApi();
    const user = userEvent.setup();
    renderPage();

    await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
    await user.click(screen.getByRole('button', { name: 'غیرفعال‌سازی موقت' }));

    expect(await screen.findByText(/یک بازهٔ مشخص را می‌بندد/)).toBeInTheDocument();
    // «الگوی کاری» فقط در همین راهنما می‌آید — نامِ تب در چند جا تکرار می‌شود.
    expect(screen.getByText(/الگوی کاری/)).toBeInTheDocument();
    expect(screen.getByRole('heading', { name: 'غیرفعال‌سازی موقت' })).toBeInTheDocument();
  });

  /**
   * حذفِ منبع عمداً در رابط کاربری نیست: منبع به نوبت‌های ثبت‌شده گره خورده و راهِ
   * درستِ «دیگر لازمش ندارم» غیرفعال کردن است.
   */
  it('تب اطلاعات کلید فعال/غیرفعال دارد و حذف منبع را نه', async () => {
    mockApi();
    renderPage();

    await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());

    // منبعِ نمونه فعال است، پس اقدام «غیرفعال کردن» پیشنهاد می‌شود.
    expect(screen.getByRole('button', { name: 'غیرفعال کردن' })).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: 'حذف' })).not.toBeInTheDocument();
    expect(screen.queryByText(/حذف منبع/)).not.toBeInTheDocument();
  });

  /** غیرفعال کردن منبع را از نوبت‌دهی بیرون می‌برد، پس بی‌تأیید انجام نمی‌شود. */
  it('غیرفعال کردن اول تأیید می‌گیرد و بدون تأیید درخواستی نمی‌فرستد', async () => {
    mockApi();
    const user = userEvent.setup();
    renderPage();

    await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
    await user.click(screen.getByRole('button', { name: 'غیرفعال کردن' }));

    expect(await screen.findByText(/در جستجوی وقت و رزرو نوبت ظاهر نمی‌شود/)).toBeInTheDocument();
    expect(patch).not.toHaveBeenCalled();

    await user.click(screen.getByRole('button', { name: 'غیرفعال کن' }));

    await waitFor(() =>
      expect(patch).toHaveBeenCalledWith('/api/v1/resource/r1', { active: false }));
  });

  /**
   * رگرسیون: جهش فقط کلیدِ فهرست (`resources`) را باطل می‌کرد، نه
   * `resource-detail` را — پس وضعیت روی همین صفحه تا رفرشِ دستی کهنه می‌ماند.
   */
  it('وضعیت بعد از غیرفعال کردن بدون رفرش به‌روز می‌شود', async () => {
    mockApi();
    const user = userEvent.setup();
    patch.mockResolvedValue({ success: true, data: { ...resource, active: false } });
    renderPage();

    await waitFor(() => expect(screen.getByText('فعال')).toBeInTheDocument());

    // پاسخِ بعدیِ سرور برای همین منبع، غیرفعال است.
    get.mockImplementation((path: string) =>
      path === '/api/v1/resource/r1'
        ? Promise.resolve({ success: true, data: { ...resource, active: false } })
        : Promise.resolve({ success: true, data: [] }));

    await user.click(screen.getByRole('button', { name: 'غیرفعال کردن' }));
    await user.click(await screen.findByRole('button', { name: 'غیرفعال کن' }));

    // بدون باطل‌کردنِ `resource-detail`، این همیشه «فعال» می‌ماند.
    expect(await screen.findByText('غیرفعال')).toBeInTheDocument();
    expect(await screen.findByRole('button', { name: 'فعال کردن' })).toBeInTheDocument();
  });
});
