import { flushPromises, mount } from '@vue/test-utils';
import { defineComponent, h, reactive } from 'vue';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { workflowFileSubmitPendingKey } from '#/components/Jnpf/upload/src/filePermissionContext';

import { refreshConfiguredFilePermissions, useFilePermissions } from './useFilePermissions';

const mocks = vi.hoisted(() => ({ get: vi.fn() }));
vi.mock('#/api/workFlow/task', () => ({ getFlowFilePermissions: mocks.get }));
const view = (taskId = 'task', revision = 'v1') => ({ enabled: true, taskId, formId: 'form', status: 'ready', revision, fields: {} });
const wrappers: ReturnType<typeof mount>[] = [];
function setup(initial = view()) {
  const config = reactive({ id: initial.taskId, formId: 'form', filePermissions: initial });
  const submission = reactive({ pending: false });
  let permissions!: ReturnType<typeof useFilePermissions>;
  const wrapper = mount(
    defineComponent({
      setup() {
        permissions = useFilePermissions(() => config);
        return () => h('div');
      },
    }),
    { global: { provide: { [workflowFileSubmitPendingKey as symbol]: () => submission.pending } } },
  );
  wrappers.push(wrapper);
  return { config, permissions, wrapper, submission };
}
beforeEach(() => {
  vi.useFakeTimers();
  mocks.get.mockReset();
});
afterEach(() => {
  wrappers.splice(0).forEach((w) => w.unmount());
  vi.useRealTimers();
});
describe('flow file permissions refresh', () => {
  it('updates configured permissions immediately after workflow state changes', async () => {
    const config = reactive({ id: 'record', formId: 'form', filePermissions: view() });
    mocks.get.mockResolvedValue({ data: view('task', 'started') });

    await expect(refreshConfiguredFilePermissions(() => config)).resolves.toBe(true);

    expect(mocks.get).toHaveBeenCalledWith('task', { formId: 'form' });
    expect(config.filePermissions.revision).toBe('started');
  });
  it('does not apply an immediate refresh response after switching records', async () => {
    let resolve!: (value: any) => void;
    mocks.get.mockReturnValueOnce(
      new Promise((done) => {
        resolve = done;
      }),
    );
    const config = reactive({ id: 'record', formId: 'form', filePermissions: view() });
    const refresh = refreshConfiguredFilePermissions(() => config);
    config.id = 'other-record';
    config.filePermissions = view('other-task', 'current');
    resolve({ data: view('task', 'stale') });

    await expect(refresh).resolves.toBe(false);
    expect(config.filePermissions.taskId).toBe('other-task');
    expect(config.filePermissions.revision).toBe('current');
  });
  it('keeps an unsaved form native and applies instance permissions after reopening the saved record', async () => {
    const config = reactive({ id: '', formId: 'form', filePermissions: undefined as ReturnType<typeof view> | undefined });
    let permissions!: ReturnType<typeof useFilePermissions>;
    wrappers.push(
      mount(
        defineComponent({
          setup() {
            permissions = useFilePermissions(() => config);
            return () => h('div');
          },
        }),
      ),
    );
    window.dispatchEvent(new Event('focus'));
    await vi.advanceTimersByTimeAsync(30_000);
    expect(permissions.value).toBeUndefined();
    expect(mocks.get).not.toHaveBeenCalled();

    config.id = 'saved-record';
    config.filePermissions = view();
    await flushPromises();
    expect(permissions.value?.enabled).toBe(true);
    mocks.get.mockRejectedValueOnce(new Error('offline'));
    window.dispatchEvent(new Event('focus'));
    await flushPromises();
    expect(mocks.get).toHaveBeenCalledWith('task', { formId: 'form' });
    expect(permissions.value?.status).toBe('error');
    expect(permissions.value?.enabled).toBe(true);

    config.id = '';
    config.filePermissions = undefined;
    await flushPromises();
    expect(permissions.value).toBeUndefined();
  });
  it('does not query old workflows or leave listeners after unmount', async () => {
    const state = setup({ ...view(), enabled: false });
    window.dispatchEvent(new Event('focus'));
    await vi.advanceTimersByTimeAsync(30_000);
    expect(mocks.get).not.toHaveBeenCalled();
    state.wrapper.unmount();
    expect(vi.getTimerCount()).toBe(0);
    window.dispatchEvent(new Event('focus'));
    expect(mocks.get).not.toHaveBeenCalled();
  });
  it('fails closed on request failure and recovers on the next focus', async () => {
    const state = setup();
    mocks.get.mockRejectedValueOnce(new Error('offline'));
    window.dispatchEvent(new Event('focus'));
    await flushPromises();
    expect(state.permissions.value?.status).toBe('error');
    mocks.get.mockResolvedValue({ data: view('task', 'v2') });
    window.dispatchEvent(new Event('focus'));
    await flushPromises();
    expect(state.permissions.value?.revision).toBe('v2');
    expect(state.permissions.value?.status).toBe('ready');
  });
  it('ignores an old response after a form switch and prevents duplicate in-flight requests', async () => {
    let resolve!: (value: any) => void;
    mocks.get.mockReturnValueOnce(
      new Promise((done) => {
        resolve = done;
      }),
    );
    const state = setup();
    window.dispatchEvent(new Event('focus'));
    window.dispatchEvent(new Event('focus'));
    expect(mocks.get).toHaveBeenCalledTimes(1);
    state.config.id = 'other';
    state.config.filePermissions = view('other');
    await flushPromises();
    mocks.get.mockResolvedValue({ data: view('other', 'new') });
    window.dispatchEvent(new Event('focus'));
    await flushPromises();
    resolve({ data: view('task', 'stale') });
    await flushPromises();
    expect(state.permissions.value?.taskId).toBe('other');
    expect(state.permissions.value?.revision).toBe('new');
  });
  it('rejects another workflow response and controlled-to-legacy downgrade', async () => {
    const state = setup();
    mocks.get.mockResolvedValue({ data: view('other') });
    window.dispatchEvent(new Event('focus'));
    await flushPromises();
    expect(state.permissions.value?.status).toBe('error');
    mocks.get.mockResolvedValue({ data: { ...view(), enabled: false } });
    window.dispatchEvent(new Event('focus'));
    await flushPromises();
    expect(state.permissions.value?.status).toBe('error');
    expect(state.permissions.value?.enabled).toBe(true);
  });
  it('refreshes using the resolved task id rather than the native record alias', async () => {
    const state = setup();
    state.config.id = 'business-record';
    await flushPromises();
    mocks.get.mockResolvedValue({ data: view() });
    window.dispatchEvent(new Event('focus'));
    await flushPromises();
    expect(mocks.get).toHaveBeenCalledWith('task', { formId: 'form' });
    expect(state.permissions.value?.status).toBe('ready');
  });
  it('fences in-flight permissions while approval submits and refreshes after failure recovery', async () => {
    const state = setup();
    let reject!: (error: Error) => void;
    mocks.get.mockReturnValueOnce(
      new Promise((_resolve, fail) => {
        reject = fail;
      }),
    );
    window.dispatchEvent(new Event('focus'));
    state.submission.pending = true;
    await vi.advanceTimersByTimeAsync(30_000);
    window.dispatchEvent(new Event('focus'));
    expect(mocks.get).toHaveBeenCalledTimes(1);
    mocks.get.mockResolvedValue({ data: view('task', 'next-node') });
    state.submission.pending = false;
    await flushPromises();
    expect(state.permissions.value?.revision).toBe('next-node');
    reject(new Error('stale transition failure'));
    await flushPromises();
    expect(state.permissions.value?.status).toBe('ready');
    expect(state.permissions.value?.revision).toBe('next-node');
  });
});
