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

import { LEGACY_PERSIST_KEY } from './legacyConnectionStorage';
import {
  bootstrapSecureConfig,
  finalizeSecurityUpdateStatus,
  mergeSecurityUpdateStatusWithLegacySource,
  startSecurityUpdateFromBootstrap,
} from './secureConfigBootstrap';
import { stripLegacyPersistedConnectionById } from './legacyConnectionStorage';

const legacyPayload = JSON.stringify({
  state: {
    connections: [
      {
        id: 'legacy-1',
        name: 'Legacy',
        config: {
          id: 'legacy-1',
          type: 'postgres',
          host: 'db.local',
          port: 5432,
          user: 'postgres',
          password: 'secret',
        },
      },
    ],
    globalProxy: {
      enabled: true,
      type: 'http',
      host: '127.0.0.1',
      port: 8080,
      user: 'ops',
      password: 'proxy-secret',
    },
  },
});

const createMemoryStorage = () => {
  const data = new Map<string, string>();
  return {
    getItem: (key: string) => data.get(key) ?? null,
    setItem: (key: string, value: string) => {
      data.set(key, value);
    },
    removeItem: (key: string) => {
      data.delete(key);
    },
  };
};

const createBaseArgs = (storage = createMemoryStorage()) => {
  const replaceConnections = vi.fn();
  const replaceGlobalProxy = vi.fn();

  storage.setItem(LEGACY_PERSIST_KEY, legacyPayload);

  return {
    storage,
    replaceConnections,
    replaceGlobalProxy,
  };
};

describe('secureConfigBootstrap', () => {
  it('builds legacy pending summary and issue list before the first round starts', async () => {
    const args = createBaseArgs();

    const result = await bootstrapSecureConfig({
      ...args,
      backend: {
        GetSecurityUpdateStatus: vi.fn().mockResolvedValue({
          overallStatus: 'not_detected',
          summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0 },
          issues: [],
        }),
      },
    });

    expect(result.status.overallStatus).toBe('pending');
    expect(result.status.summary).toEqual({
      total: 2,
      updated: 0,
      pending: 2,
      skipped: 0,
      failed: 0,
    });
    expect(result.status.issues).toEqual(expect.arrayContaining([
      expect.objectContaining({
        scope: 'connection',
        refId: 'legacy-1',
        action: 'open_connection',
      }),
      expect.objectContaining({
        scope: 'global_proxy',
        action: 'open_proxy_settings',
      }),
    ]));
  });

  it('shows intro when legacy sensitive items exist and backend status is pending', async () => {
    const args = createBaseArgs();

    const result = await bootstrapSecureConfig({
      ...args,
      backend: {
        GetSecurityUpdateStatus: vi.fn().mockResolvedValue({
          overallStatus: 'pending',
          summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0 },
          issues: [],
        }),
      },
    });

    expect(result.status.overallStatus).toBe('pending');
    expect(result.shouldShowIntro).toBe(true);
    expect(result.shouldShowBanner).toBe(false);
    expect(args.replaceConnections).toHaveBeenCalledWith(
      expect.arrayContaining([expect.objectContaining({ id: 'legacy-1' })]),
    );
  });

  it('keeps banner flow without intro when backend status is postponed', async () => {
    const args = createBaseArgs();

    const result = await bootstrapSecureConfig({
      ...args,
      backend: {
        GetSecurityUpdateStatus: vi.fn().mockResolvedValue({
          overallStatus: 'postponed',
          summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0 },
          issues: [],
        }),
      },
    });

    expect(result.shouldShowIntro).toBe(false);
    expect(result.shouldShowBanner).toBe(true);
  });

  it('keeps legacy pending summary and issues when a pre-start round is postponed', async () => {
    const args = createBaseArgs();

    const result = await bootstrapSecureConfig({
      ...args,
      backend: {
        GetSecurityUpdateStatus: vi.fn().mockResolvedValue({
          overallStatus: 'postponed',
          summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0 },
          issues: [],
        }),
      },
    });

    expect(result.status.overallStatus).toBe('postponed');
    expect(result.status.summary.total).toBe(2);
    expect(result.status.summary.pending).toBe(2);
    expect(result.status.issues).toEqual(expect.arrayContaining([
      expect.objectContaining({ scope: 'connection', refId: 'legacy-1' }),
      expect.objectContaining({ scope: 'global_proxy' }),
    ]));
  });

  it('merges backend pending issues with legacy source items before the first round starts', async () => {
    const args = createBaseArgs();

    const result = await bootstrapSecureConfig({
      ...args,
      backend: {
        GetSecurityUpdateStatus: vi.fn().mockResolvedValue({
          overallStatus: 'pending',
          summary: { total: 1, updated: 0, pending: 1, skipped: 0, failed: 0 },
          issues: [
            {
              id: 'ai-provider-openai-main',
              scope: 'ai_provider',
              refId: 'openai-main',
              title: 'OpenAI',
              severity: 'medium',
              status: 'pending',
              reasonCode: 'secret_missing',
              action: 'open_ai_settings',
              message: 'AI 提供商配置仍需完成安全更新',
            },
          ],
        }),
      },
    });

    expect(result.status.overallStatus).toBe('pending');
    expect(result.status.summary).toEqual({
      total: 3,
      updated: 0,
      pending: 3,
      skipped: 0,
      failed: 0,
    });
    expect(result.status.issues).toEqual(expect.arrayContaining([
      expect.objectContaining({ scope: 'ai_provider', refId: 'openai-main' }),
      expect.objectContaining({ scope: 'connection', refId: 'legacy-1' }),
      expect.objectContaining({ scope: 'global_proxy' }),
    ]));
  });

  it('keeps banner flow without intro when backend status is rolled_back', async () => {
    const args = createBaseArgs();

    const result = await bootstrapSecureConfig({
      ...args,
      backend: {
        GetSecurityUpdateStatus: vi.fn().mockResolvedValue({
          overallStatus: 'rolled_back',
          summary: { total: 1, updated: 0, pending: 0, skipped: 0, failed: 1 },
          issues: [],
        }),
      },
    });

    expect(result.shouldShowIntro).toBe(false);
    expect(result.shouldShowBanner).toBe(true);
  });

  it('merges legacy pending items into rolled_back status without overwriting backend system issues', () => {
    const status = mergeSecurityUpdateStatusWithLegacySource({
      overallStatus: 'rolled_back',
      summary: { total: 1, updated: 0, pending: 0, skipped: 0, failed: 1 },
      issues: [
        {
          id: 'system-blocked',
          scope: 'system',
          title: '系统回滚',
          severity: 'high',
          status: 'failed',
          reasonCode: 'environment_blocked',
          action: 'view_details',
          message: '后端已回滚本轮更新，需要处理后重试。',
        },
      ],
    }, legacyPayload);

    expect(status.overallStatus).toBe('rolled_back');
    expect(status.summary).toEqual({
      total: 3,
      updated: 0,
      pending: 2,
      skipped: 0,
      failed: 1,
    });
    expect(status.issues).toEqual(expect.arrayContaining([
      expect.objectContaining({ id: 'system-blocked', scope: 'system' }),
      expect.objectContaining({ id: 'legacy-connection-legacy-1', scope: 'connection', refId: 'legacy-1' }),
      expect.objectContaining({ id: 'legacy-global-proxy-default', scope: 'global_proxy' }),
    ]));
  });

  it('merges legacy pending items into needs_attention status without overwriting backend system issues', () => {
    const status = mergeSecurityUpdateStatusWithLegacySource({
      overallStatus: 'needs_attention',
      summary: { total: 2, updated: 1, pending: 0, skipped: 0, failed: 1 },
      issues: [
        {
          id: 'system-partial-failure',
          scope: 'system',
          title: '部分失败',
          severity: 'high',
          status: 'failed',
          reasonCode: 'environment_blocked',
          action: 'view_details',
          message: '部分项目迁移失败，需要人工处理。',
        },
        {
          id: 'ai-provider-openai-main',
          scope: 'ai_provider',
          refId: 'openai-main',
          title: 'OpenAI',
          severity: 'medium',
          status: 'updated',
          action: 'open_ai_settings',
          message: 'AI 提供商配置已完成安全更新。',
        },
      ],
    }, legacyPayload);

    expect(status.overallStatus).toBe('needs_attention');
    expect(status.summary).toEqual({
      total: 4,
      updated: 1,
      pending: 2,
      skipped: 0,
      failed: 1,
    });
    expect(status.issues).toEqual(expect.arrayContaining([
      expect.objectContaining({ id: 'system-partial-failure', scope: 'system' }),
      expect.objectContaining({ id: 'ai-provider-openai-main', scope: 'ai_provider', refId: 'openai-main' }),
      expect.objectContaining({ id: 'legacy-connection-legacy-1', scope: 'connection', refId: 'legacy-1' }),
      expect.objectContaining({ id: 'legacy-global-proxy-default', scope: 'global_proxy' }),
    ]));
  });

  it('does not merge local legacy pending items back into an active migration round that already reports needs_attention', () => {
    const status = mergeSecurityUpdateStatusWithLegacySource({
      migrationId: 'migration-active-1',
      overallStatus: 'needs_attention',
      summary: { total: 3, updated: 2, pending: 1, skipped: 0, failed: 0 },
      issues: [
        {
          id: 'ai-provider-openai-main',
          scope: 'ai_provider',
          refId: 'openai-main',
          title: 'OpenAI',
          severity: 'medium',
          status: 'needs_attention',
          reasonCode: 'secret_missing',
          action: 'open_ai_settings',
          message: 'AI 提供商配置需要补充后才能完成安全更新。',
        },
      ],
    }, legacyPayload);

    expect(status.overallStatus).toBe('needs_attention');
    expect(status.summary).toEqual({
      total: 3,
      updated: 2,
      pending: 1,
      skipped: 0,
      failed: 0,
    });
    expect(status.issues).toEqual([
      expect.objectContaining({ id: 'ai-provider-openai-main', scope: 'ai_provider', refId: 'openai-main' }),
    ]);
  });

  it('does not merge local legacy pending items back into a rolled_back migration round', () => {
    const status = mergeSecurityUpdateStatusWithLegacySource({
      migrationId: 'migration-active-2',
      overallStatus: 'rolled_back',
      summary: { total: 3, updated: 1, pending: 0, skipped: 0, failed: 2 },
      issues: [
        {
          id: 'system-blocked',
          scope: 'system',
          title: '系统回滚',
          severity: 'high',
          status: 'failed',
          reasonCode: 'environment_blocked',
          action: 'view_details',
          message: '后端已回滚本轮更新，需要处理后重试。',
        },
      ],
    }, legacyPayload);

    expect(status.overallStatus).toBe('rolled_back');
    expect(status.summary).toEqual({
      total: 3,
      updated: 1,
      pending: 0,
      skipped: 0,
      failed: 2,
    });
    expect(status.issues).toEqual([
      expect.objectContaining({ id: 'system-blocked', scope: 'system' }),
    ]);
  });

  it('loads backend secure config directly when no legacy source exists', async () => {
    const storage = createMemoryStorage();
    const replaceConnections = vi.fn();
    const replaceGlobalProxy = vi.fn();

    const result = await bootstrapSecureConfig({
      storage,
      replaceConnections,
      replaceGlobalProxy,
      backend: {
        GetSecurityUpdateStatus: vi.fn().mockResolvedValue({
          overallStatus: 'not_detected',
          summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0 },
          issues: [],
        }),
        GetSavedConnections: vi.fn().mockResolvedValue([
          {
            id: 'secure-1',
            name: 'Secure',
            config: {
              id: 'secure-1',
              type: 'postgres',
              host: 'db.local',
              port: 5432,
              user: 'postgres',
            },
          },
        ]),
      },
    });

    expect(result.status.overallStatus).toBe('not_detected');
    expect(replaceConnections).toHaveBeenCalledWith(
      expect.arrayContaining([expect.objectContaining({ id: 'secure-1' })]),
    );
  });

  it('shows intro when backend status is pending even without legacy local source', async () => {
    const storage = createMemoryStorage();
    const replaceConnections = vi.fn();
    const replaceGlobalProxy = vi.fn();

    const result = await bootstrapSecureConfig({
      storage,
      replaceConnections,
      replaceGlobalProxy,
      backend: {
        GetSecurityUpdateStatus: vi.fn().mockResolvedValue({
          overallStatus: 'pending',
          summary: { total: 1, updated: 0, pending: 1, skipped: 0, failed: 0 },
          issues: [],
        }),
      },
    });

    expect(result.status.overallStatus).toBe('pending');
    expect(result.shouldShowIntro).toBe(true);
    expect(result.shouldShowBanner).toBe(false);
  });

  it('falls back to legacy visible config when StartSecurityUpdate throws', async () => {
    const args = createBaseArgs();

    const result = await startSecurityUpdateFromBootstrap({
      ...args,
      backend: {
        StartSecurityUpdate: vi.fn().mockRejectedValue(new Error('boom')),
      },
    });

    expect(result.status).toBeNull();
    expect(result.error?.message).toContain('boom');
    expect(args.replaceConnections).toHaveBeenCalledWith(
      expect.arrayContaining([expect.objectContaining({ id: 'legacy-1' })]),
    );
    expect(args.storage.getItem(LEGACY_PERSIST_KEY)).toContain('"password":"secret"');
  });

  it('starts security update even when rawPayload is empty but backend supports AI-only update', async () => {
    const storage = createMemoryStorage();
    const replaceConnections = vi.fn();
    const replaceGlobalProxy = vi.fn();
    const StartSecurityUpdate = vi.fn().mockResolvedValue({
      overallStatus: 'completed',
      summary: { total: 1, updated: 1, pending: 0, skipped: 0, failed: 0 },
      issues: [],
    });

    const result = await startSecurityUpdateFromBootstrap({
      storage,
      replaceConnections,
      replaceGlobalProxy,
      backend: {
        StartSecurityUpdate,
      },
    });

    expect(result.error).toBeNull();
    expect(result.status?.overallStatus).toBe('completed');
    expect(StartSecurityUpdate).toHaveBeenCalledWith({
      sourceType: 'current_app_saved_config',
      rawPayload: '',
      options: {
        allowPartial: true,
        writeBackup: true,
      },
    });
  });

  it('keeps source-side secrets when update ends in needs_attention', async () => {
    const args = createBaseArgs();

    const result = await startSecurityUpdateFromBootstrap({
      ...args,
      backend: {
        StartSecurityUpdate: vi.fn().mockResolvedValue({
          overallStatus: 'needs_attention',
          summary: { total: 3, updated: 2, pending: 1, skipped: 0, failed: 0 },
          issues: [{ id: 'ai-1' }],
        }),
        GetSavedConnections: vi.fn().mockResolvedValue([]),
      },
    });

    expect(result.status?.overallStatus).toBe('needs_attention');
    expect(args.storage.getItem(LEGACY_PERSIST_KEY)).toContain('"password":"secret"');
  });

  it('cleans source-side secrets only after completed update and backend refresh', async () => {
    const args = createBaseArgs();

    const result = await startSecurityUpdateFromBootstrap({
      ...args,
      backend: {
        StartSecurityUpdate: vi.fn().mockResolvedValue({
          overallStatus: 'completed',
          summary: { total: 3, updated: 3, pending: 0, skipped: 0, failed: 0 },
          issues: [],
        }),
        GetSavedConnections: vi.fn().mockResolvedValue([
          {
            id: 'secure-1',
            name: 'Secure',
            config: {
              id: 'secure-1',
              type: 'postgres',
              host: 'db.local',
              port: 5432,
              user: 'postgres',
            },
            hasPrimaryPassword: true,
          },
        ]),
        GetGlobalProxyConfig: vi.fn().mockResolvedValue({
          success: true,
          data: {
            enabled: true,
            type: 'http',
            host: '127.0.0.1',
            port: 8080,
            user: 'ops',
            hasPassword: true,
          },
        }),
      },
    });

    expect(result.status?.overallStatus).toBe('completed');
    expect(args.storage.getItem(LEGACY_PERSIST_KEY)).not.toContain('"password":"secret"');
    expect(args.replaceConnections).toHaveBeenLastCalledWith(
      expect.arrayContaining([expect.objectContaining({ id: 'secure-1' })]),
    );
  });

  it('refreshes backend config and strips source-side secrets when a later round finishes as completed', async () => {
    const args = createBaseArgs();

    const status = await finalizeSecurityUpdateStatus({
      ...args,
      backend: {
        GetSavedConnections: vi.fn().mockResolvedValue([
          {
            id: 'secure-1',
            name: 'Secure',
            config: {
              id: 'secure-1',
              type: 'postgres',
              host: 'db.local',
              port: 5432,
              user: 'postgres',
            },
            hasPrimaryPassword: true,
          },
        ]),
        GetGlobalProxyConfig: vi.fn().mockResolvedValue({
          success: true,
          data: {
            enabled: true,
            type: 'http',
            host: '127.0.0.1',
            port: 8080,
            user: 'ops',
            hasPassword: true,
          },
        }),
      },
    }, {
      overallStatus: 'completed',
      summary: { total: 3, updated: 3, pending: 0, skipped: 0, failed: 0 },
      issues: [],
    });

    expect(status.overallStatus).toBe('completed');
    expect(args.storage.getItem(LEGACY_PERSIST_KEY)).not.toContain('"password":"secret"');
    expect(args.replaceConnections).toHaveBeenLastCalledWith(
      expect.arrayContaining([expect.objectContaining({ id: 'secure-1' })]),
    );
  });

  it('reduces legacy pending issues after a single connection is repaired before the first round starts', () => {
    const initialStatus = mergeSecurityUpdateStatusWithLegacySource({
      overallStatus: 'not_detected',
      summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0 },
      issues: [],
    }, legacyPayload);
    const nextPayload = stripLegacyPersistedConnectionById(legacyPayload, 'legacy-1');

    const status = mergeSecurityUpdateStatusWithLegacySource({
      overallStatus: 'not_detected',
      summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0 },
      issues: [],
    }, nextPayload, {
      previousStatus: initialStatus,
    });

    expect(status.overallStatus).toBe('pending');
    expect(status.summary).toEqual({
      total: 2,
      updated: 1,
      pending: 1,
      skipped: 0,
      failed: 0,
    });
    expect(status.issues).toEqual([
      expect.objectContaining({
        scope: 'global_proxy',
        action: 'open_proxy_settings',
      }),
    ]);
  });

  it('accumulates pre-start repaired progress across multiple connection saves in the same round-free session', () => {
    const multiConnectionPayload = JSON.stringify({
      state: {
        connections: [
          {
            id: 'legacy-1',
            name: 'Legacy 1',
            config: {
              id: 'legacy-1',
              type: 'postgres',
              host: 'db-1.local',
              port: 5432,
              user: 'postgres',
              password: 'secret-1',
            },
          },
          {
            id: 'legacy-2',
            name: 'Legacy 2',
            config: {
              id: 'legacy-2',
              type: 'postgres',
              host: 'db-2.local',
              port: 5432,
              user: 'postgres',
              password: 'secret-2',
            },
          },
          {
            id: 'legacy-3',
            name: 'Legacy 3',
            config: {
              id: 'legacy-3',
              type: 'postgres',
              host: 'db-3.local',
              port: 5432,
              user: 'postgres',
              password: 'secret-3',
            },
          },
        ],
      },
    });

    const backendStatus = {
      overallStatus: 'not_detected' as const,
      summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0 },
      issues: [],
    };
    const initialStatus = mergeSecurityUpdateStatusWithLegacySource(backendStatus, multiConnectionPayload);
    const afterFirstRepairPayload = stripLegacyPersistedConnectionById(multiConnectionPayload, 'legacy-1');
    const afterFirstRepairStatus = mergeSecurityUpdateStatusWithLegacySource(backendStatus, afterFirstRepairPayload, {
      previousStatus: initialStatus,
    });
    const afterSecondRepairPayload = stripLegacyPersistedConnectionById(afterFirstRepairPayload, 'legacy-2');

    const afterSecondRepairStatus = mergeSecurityUpdateStatusWithLegacySource(backendStatus, afterSecondRepairPayload, {
      previousStatus: afterFirstRepairStatus,
    });

    expect(afterFirstRepairStatus.summary).toEqual({
      total: 3,
      updated: 1,
      pending: 2,
      skipped: 0,
      failed: 0,
    });
    expect(afterSecondRepairStatus.summary).toEqual({
      total: 3,
      updated: 2,
      pending: 1,
      skipped: 0,
      failed: 0,
    });
    expect(afterSecondRepairStatus.issues).toEqual([
      expect.objectContaining({
        id: 'legacy-connection-legacy-3',
        scope: 'connection',
        refId: 'legacy-3',
      }),
    ]);
  });
});
