import type { Router } from 'vue-router';

/* eslint-disable vue/one-component-per-file -- Inline stubs keep this publish-flow test isolated. */
import { flushPromises, mount } from '@vue/test-utils';
import { createMemoryHistory, createRouter } from 'vue-router';

import { ElMessage, ElMessageBox } from 'element-plus';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import SkillDetail from './SkillDetail.vue';

const apiMocks = vi.hoisted(() => ({
  getSkillCategories: vi.fn(),
  getSkillDetail: vi.fn(),
  saveSkill: vi.fn(),
  submitSkillDeleteApproval: vi.fn(),
  submitSkillOfflineApproval: vi.fn(),
  submitSkillPublishApproval: vi.fn(),
  updateSkill: vi.fn(),
}));

const childMocks = vi.hoisted(() => ({
  capabilityHasDirty: vi.fn(() => false),
  capabilitySave: vi.fn(),
  resourceSave: vi.fn(async () => true),
  resourceValidate: vi.fn(),
}));
const permissionMocks = vi.hoisted(() => ({
  hasPermission: vi.fn((_permissions: string[]) => true),
}));

vi.mock('vue-router', async (importOriginal) => {
  const original = await importOriginal<typeof import('vue-router')>();
  return {
    ...original,
    onBeforeRouteLeave: vi.fn(),
    onBeforeRouteUpdate: vi.fn(),
  };
});
vi.mock('./api', () => apiMocks);
vi.mock('#/api/common/hasPermission', () => ({
  hasPermission: permissionMocks.hasPermission,
}));
vi.mock('./SkillResourceWorkbench.vue', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    default: defineComponent({
      name: 'SkillResourceWorkbench',
      props: {
        activePanel: { default: 'resources', type: String },
        capabilityAvailable: Boolean,
        readonly: Boolean,
      },
      emits: [
        'dirty',
        'issues',
        'locateCapability',
        'newContent',
        'requestSave',
        'update:activePanel',
      ],
      setup(props, { expose, slots }) {
        expose({
          getSkillContent: () => undefined,
          saveAll: childMocks.resourceSave,
          validateAll: childMocks.resourceValidate,
        });
        return () =>
          h(
            'div',
            {
              'data-readonly': String(props.readonly),
              'data-testid': 'resource-workbench',
            },
            slots.capability?.(),
          );
      },
    }),
  };
});
vi.mock('./SkillSettingsDialog.vue', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    default: defineComponent({
      name: 'SkillSettingsDialog',
      props: {
        categoryLoadError: { default: '', type: String },
        modelValue: Boolean,
      },
      emits: ['retryCategories', 'saved', 'update:modelValue'],
      setup(props, { emit }) {
        return () =>
          props.modelValue
            ? h('div', { 'data-testid': 'settings-dialog' }, [
                props.categoryLoadError,
                h('button', { onClick: () => emit('retryCategories') }, '重试'),
              ])
            : null;
      },
    }),
  };
});
vi.mock('./SkillCapabilityPanel.vue', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    default: defineComponent({
      name: 'SkillCapabilityPanel',
      props: { locked: Boolean, readonly: Boolean },
      setup(props, { expose }) {
        expose({
          flushPending: childMocks.capabilitySave,
          hasDirty: childMocks.capabilityHasDirty,
        });
        return () =>
          h('div', {
            'data-locked': String(props.locked),
            'data-testid': 'capability-panel',
          });
      },
    }),
  };
});

let router: Router;

async function mountDetail(id = '101', navTitle?: string) {
  router = createRouter({
    history: createMemoryHistory(),
    routes: [
      { component: { template: '<div />' }, path: '/ai/skill' },
      {
        component: { template: '<div />' },
        path: '/ai/skill/detail/:id',
      },
    ],
  });
  await router.push({
    path: `/ai/skill/detail/${id}`,
    query: navTitle ? { navTitle } : undefined,
  });
  await router.isReady();
  const wrapper = mount(SkillDetail, {
    attachTo: document.body,
    global: {
      directives: { access: {}, loading: {} },
      plugins: [router],
    },
  });
  await flushPromises();
  return wrapper;
}

function publishButton(wrapper: ReturnType<typeof mount>) {
  const button = wrapper
    .findAll('button')
    .find((candidate) => candidate.text().trim() === '发布');
  if (!button) throw new Error('未找到发布按钮');
  return button;
}

function markResourceDirty(wrapper: ReturnType<typeof mount>) {
  wrapper
    .getComponent({ name: 'SkillResourceWorkbench' })
    .vm.$emit('dirty', true);
}

describe('skill detail publish transaction', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    childMocks.capabilityHasDirty.mockReset().mockReturnValue(false);
    childMocks.capabilitySave.mockReset().mockResolvedValue(true);
    childMocks.resourceSave.mockReset().mockResolvedValue(true);
    childMocks.resourceValidate.mockReset().mockResolvedValue(true);
    permissionMocks.hasPermission.mockReset().mockReturnValue(true);
    vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
      callback(0);
      return 1;
    });
    apiMocks.getSkillCategories.mockResolvedValue({ data: [], errorCode: 0 });
    apiMocks.getSkillDetail.mockResolvedValue({
      data: {
        capabilityHash: 'a'.repeat(64),
        description: 'Demonstration skill',
        displayName: '演示 Skill',
        displayPublishStatus: 'DRAFT',
        enabled: true,
        id: 101,
        manageable: true,
        name: 'demo-skill',
        publishStatus: 'DRAFT',
        skillContent:
          '---\nname: demo-skill\ndescription: Demonstration skill\n---\n\n# Instructions\n',
        visibilityScope: 'PRIVATE',
      },
      errorCode: 0,
    });
  });

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

  it('locks editors and follows validate, confirm, submit order', async () => {
    const calls: string[] = [];
    childMocks.resourceSave.mockImplementation(async () => {
      calls.push('save');
      return true;
    });
    let resolveValidation: ((value: boolean) => void) | undefined;
    let resolveSubmit:
      | ((value: { data: number; errorCode: number; message: string }) => void)
      | undefined;
    childMocks.resourceValidate.mockImplementation(
      () =>
        new Promise<boolean>((resolve) => {
          calls.push('validate');
          resolveValidation = resolve;
        }),
    );
    vi.spyOn(ElMessageBox, 'confirm').mockImplementation(async () => {
      calls.push('confirm');
      return { action: 'confirm' } as Awaited<
        ReturnType<typeof ElMessageBox.confirm>
      >;
    });
    apiMocks.submitSkillPublishApproval.mockImplementation(
      () =>
        new Promise((resolve) => {
          calls.push('submit');
          resolveSubmit = resolve;
        }),
    );
    const wrapper = await mountDetail();
    markResourceDirty(wrapper);
    await flushPromises();

    await publishButton(wrapper).trigger('click');
    await flushPromises();

    expect(calls).toEqual(['save', 'validate']);
    expect(
      wrapper
        .get('[data-testid="resource-workbench"]')
        .attributes('data-readonly'),
    ).toBe('true');
    expect(
      wrapper.get('[data-testid="capability-panel"]').attributes('data-locked'),
    ).toBe('true');

    resolveValidation?.(true);
    await flushPromises();

    expect(calls).toEqual(['save', 'validate', 'confirm', 'submit']);
    expect(childMocks.resourceSave).toHaveBeenCalledOnce();
    expect(childMocks.resourceValidate).toHaveBeenCalledWith();

    resolveSubmit?.({ data: 9001, errorCode: 0, message: '已提交' });
    await flushPromises();

    expect(
      wrapper
        .get('[data-testid="resource-workbench"]')
        .attributes('data-readonly'),
    ).toBe('false');
    expect(
      wrapper.get('[data-testid="capability-panel"]').attributes('data-locked'),
    ).toBe('false');
    wrapper.unmount();
  });

  it('does not confirm or submit when publish validation fails', async () => {
    childMocks.resourceValidate.mockResolvedValue(false);
    const confirm = vi.spyOn(ElMessageBox, 'confirm');
    const wrapper = await mountDetail();

    await publishButton(wrapper).trigger('click');
    await flushPromises();

    expect(childMocks.resourceValidate).toHaveBeenCalledWith();
    expect(confirm).not.toHaveBeenCalled();
    expect(apiMocks.submitSkillPublishApproval).not.toHaveBeenCalled();
    wrapper.unmount();
  });

  it('stops before validation when saving dirty resources fails', async () => {
    childMocks.resourceSave.mockResolvedValue(false);
    const confirm = vi.spyOn(ElMessageBox, 'confirm');
    const wrapper = await mountDetail();
    markResourceDirty(wrapper);
    await flushPromises();

    await publishButton(wrapper).trigger('click');
    await flushPromises();

    expect(childMocks.resourceSave).toHaveBeenCalledOnce();
    expect(childMocks.resourceValidate).not.toHaveBeenCalled();
    expect(confirm).not.toHaveBeenCalled();
    expect(apiMocks.submitSkillPublishApproval).not.toHaveBeenCalled();
    wrapper.unmount();
  });

  it('uses one resource and capability workspace and opens settings on demand', async () => {
    const wrapper = await mountDetail();
    const workbench = wrapper.getComponent({ name: 'SkillResourceWorkbench' });

    expect(wrapper.findAll('[role="tab"]')).toHaveLength(0);
    expect(workbench.props('activePanel')).toBe('resources');
    expect(workbench.props('capabilityAvailable')).toBe(true);
    expect(wrapper.find('[data-testid="capability-panel"]').exists()).toBe(
      true,
    );

    expect(wrapper.text()).not.toContain('设置');
    expect(wrapper.text()).not.toContain('校验');
    const lifecycleDropdown = wrapper.findComponent({ name: 'ElDropdown' });
    expect(lifecycleDropdown.props('trigger')).toBe('click');
    lifecycleDropdown.vm.$emit('command', 'settings');
    await flushPromises();
    expect(wrapper.find('[data-testid="settings-dialog"]').exists()).toBe(true);
    wrapper.unmount();
  });

  it('keeps the routed display title when an older detail response omits it', async () => {
    apiMocks.getSkillDetail.mockResolvedValueOnce({
      data: {
        capabilityHash: 'a'.repeat(64),
        displayName: '',
        displayPublishStatus: 'DRAFT',
        manageable: true,
        name: '',
        publishStatus: 'DRAFT',
        skillContent:
          '---\nname: demo-skill\ndescription: Demonstration skill\n---\n\n# Instructions\n',
      },
      errorCode: 0,
    });
    const wrapper = await mountDetail('101', '验收 Skill');

    expect(wrapper.get('h1').text()).toBe('验收 Skill');
    wrapper.unmount();
  });

  it('keeps one file save action and handles workbench and keyboard save requests', async () => {
    const wrapper = await mountDetail();
    const saveButtons = wrapper
      .findAll('button')
      .filter((button) => button.text().trim() === '保存');
    expect(saveButtons).toHaveLength(1);
    expect(saveButtons[0]?.attributes('disabled')).toBeDefined();

    markResourceDirty(wrapper);
    await flushPromises();
    expect(saveButtons[0]?.attributes('disabled')).toBeUndefined();

    wrapper
      .getComponent({ name: 'SkillResourceWorkbench' })
      .vm.$emit('requestSave');
    await flushPromises();
    expect(childMocks.resourceSave).toHaveBeenCalledOnce();

    markResourceDirty(wrapper);
    await flushPromises();
    const shortcut = new KeyboardEvent('keydown', {
      bubbles: true,
      cancelable: true,
      key: 's',
      metaKey: true,
    });
    window.dispatchEvent(shortcut);
    await flushPromises();

    expect(shortcut.defaultPrevented).toBe(true);
    expect(childMocks.resourceSave).toHaveBeenCalledTimes(2);
    wrapper.unmount();
  });

  it('redirects the retired direct-new route back to the Skill list', async () => {
    const wrapper = await mountDetail('new');

    expect(router.currentRoute.value.path).toBe('/ai/skill');
    wrapper.unmount();
  });

  it('keeps validation navigation on resources without capability permission', async () => {
    permissionMocks.hasPermission.mockImplementation(
      (permissions: string[]) =>
        !permissions.includes('/api/v1/skill/capability'),
    );
    const warning = vi.spyOn(ElMessage, 'warning');
    const wrapper = await mountDetail();

    const workbench = wrapper.getComponent({ name: 'SkillResourceWorkbench' });
    workbench.vm.$emit('locateCapability');
    await flushPromises();

    expect(workbench.props('activePanel')).toBe('resources');
    expect(workbench.props('capabilityAvailable')).toBe(false);
    expect(wrapper.find('[data-testid="capability-panel"]').exists()).toBe(
      false,
    );
    expect(warning).toHaveBeenCalledWith('当前没有查看能力绑定的权限');
    wrapper.unmount();
  });

  it('distinguishes a category request failure from an unavailable category and supports retry', async () => {
    apiMocks.getSkillCategories.mockRejectedValueOnce(new Error('network'));
    apiMocks.getSkillDetail.mockResolvedValueOnce({
      data: {
        categoryId: 77,
        categoryName: '研发工具',
        description: 'Demonstration skill',
        displayName: '演示 Skill',
        displayPublishStatus: 'DRAFT',
        enabled: true,
        id: 101,
        manageable: true,
        name: 'demo-skill',
        publishStatus: 'DRAFT',
        skillContent:
          '---\nname: demo-skill\ndescription: Demonstration skill\n---\n\n# Instructions\n',
        visibilityScope: 'PRIVATE',
      },
      errorCode: 0,
    });
    const wrapper = await mountDetail();

    wrapper
      .findComponent({ name: 'ElDropdown' })
      .vm.$emit('command', 'settings');
    await flushPromises();
    expect(wrapper.get('[data-testid="settings-dialog"]').text()).toContain(
      '分类加载失败，请重试',
    );

    apiMocks.getSkillCategories.mockResolvedValueOnce({
      data: [{ categoryName: '研发工具', id: 77, status: 1 }],
      errorCode: 0,
    });
    await wrapper
      .get('[data-testid="settings-dialog"] button')
      .trigger('click');
    await flushPromises();

    expect(apiMocks.getSkillCategories).toHaveBeenCalledTimes(2);
    expect(wrapper.text()).not.toContain('分类加载失败，请重试');
    wrapper.unmount();
  });
});
