/**
 * Component: Setup Validation API Route Tests
 * Documentation: documentation/testing.md
 */

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

const setupGuardPrismaMock = vi.hoisted(() => ({
  configuration: {
    findUnique: vi.fn(), // returns undefined by default = setup not complete
  },
}));

const plexServiceMock = vi.hoisted(() => ({
  testConnection: vi.fn(),
  getLibraries: vi.fn(),
}));
const qbtMock = vi.hoisted(() => ({
  testConnectionWithCredentials: vi.fn(),
}));
const sabnzbdMock = vi.hoisted(() => ({
  testConnection: vi.fn(),
}));
const prowlarrMock = vi.hoisted(() => ({
  getIndexers: vi.fn(),
}));
const issuerMock = vi.hoisted(() => ({
  discover: vi.fn(),
}));
const fsMock = vi.hoisted(() => ({
  access: vi.fn(),
  mkdir: vi.fn(),
  writeFile: vi.fn(),
  unlink: vi.fn(),
}));
const configServiceMock = vi.hoisted(() => ({
  get: vi.fn(),
}));
const downloadClientManagerMock = vi.hoisted(() => ({
  testConnection: vi.fn(),
}));

vi.mock('@/lib/integrations/plex.service', () => ({
  getPlexService: () => plexServiceMock,
}));

vi.mock('@/lib/integrations/qbittorrent.service', () => ({
  QBittorrentService: {
    testConnectionWithCredentials: qbtMock.testConnectionWithCredentials,
  },
}));

vi.mock('@/lib/integrations/sabnzbd.service', () => ({
  SABnzbdService: class {
    constructor() {}
    testConnection = sabnzbdMock.testConnection;
  },
}));

vi.mock('@/lib/integrations/transmission.service', () => ({
  TransmissionService: class {
    constructor() {}
    testConnection = vi.fn();
  },
}));

vi.mock('@/lib/integrations/prowlarr.service', () => ({
  ProwlarrService: class {
    constructor() {}
    getIndexers = prowlarrMock.getIndexers;
  },
}));

vi.mock('openid-client', () => ({
  Issuer: issuerMock,
}));

vi.mock('@/lib/db', () => ({
  prisma: setupGuardPrismaMock,
}));

vi.mock('fs/promises', () => ({ default: fsMock, ...fsMock, constants: { R_OK: 4 } }));

vi.mock('@/lib/services/config.service', () => ({
  getConfigService: () => configServiceMock,
}));

vi.mock('@/lib/services/download-client-manager.service', () => ({
  getDownloadClientManager: () => downloadClientManagerMock,
}));

describe('Setup test routes', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('validates Plex connection and returns libraries', async () => {
    plexServiceMock.testConnection.mockResolvedValue({
      success: true,
      info: { platform: 'Plex', version: '1.0', machineIdentifier: 'machine' },
    });
    plexServiceMock.getLibraries.mockResolvedValue([{ id: '1', title: 'Books', type: 'book' }]);

    const { POST } = await import('@/app/api/setup/test-plex/route');
    const response = await POST({ json: vi.fn().mockResolvedValue({ url: 'http://plex', token: 'token' }) } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.libraries[0].id).toBe('1');
  });

  it('returns 400 when Plex url or token is missing', async () => {
    const { POST } = await import('@/app/api/setup/test-plex/route');
    const response = await POST({ json: vi.fn().mockResolvedValue({ url: 'http://plex' }) } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/URL and token/);
  });

  it('returns 400 when Plex connection fails', async () => {
    plexServiceMock.testConnection.mockResolvedValue({
      success: false,
      message: 'bad token',
    });

    const { POST } = await import('@/app/api/setup/test-plex/route');
    const response = await POST({ json: vi.fn().mockResolvedValue({ url: 'http://plex', token: 'bad' }) } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/bad token/);
  });

  it('returns 400 when Plex info is missing', async () => {
    plexServiceMock.testConnection.mockResolvedValue({
      success: true,
      info: null,
      message: 'missing info',
    });

    const { POST } = await import('@/app/api/setup/test-plex/route');
    const response = await POST({ json: vi.fn().mockResolvedValue({ url: 'http://plex', token: 'token' }) } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/missing info/);
  });

  it('returns 500 when Plex test throws', async () => {
    plexServiceMock.testConnection.mockRejectedValue(new Error('connection error'));

    const { POST } = await import('@/app/api/setup/test-plex/route');
    const response = await POST({ json: vi.fn().mockResolvedValue({ url: 'http://plex', token: 'token' }) } as any);
    const payload = await response.json();

    expect(response.status).toBe(500);
    expect(payload.error).toMatch(/connection error/);
  });

  it('tests qBittorrent credentials', async () => {
    downloadClientManagerMock.testConnection.mockResolvedValueOnce({
      success: true,
      message: 'Successfully connected to qBittorrent (v4.0.0)',
    });

    const { POST } = await import('@/app/api/setup/test-download-client/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        type: 'qbittorrent',
        url: 'http://qbt',
        username: 'user',
        password: 'pass',
      }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.message).toContain('4.0.0');
  });

  it('rejects invalid download client type', async () => {
    const { POST } = await import('@/app/api/setup/test-download-client/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({ type: 'rtorrent', url: 'http://rtorrent' }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/Invalid client type/);
  });

  it('tests Transmission credentials', async () => {
    downloadClientManagerMock.testConnection.mockResolvedValueOnce({
      success: true,
      message: 'Successfully connected to Transmission (v4.0.5)',
    });

    const { POST } = await import('@/app/api/setup/test-download-client/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        type: 'transmission',
        url: 'http://transmission:9091',
        username: 'user',
        password: 'pass',
      }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.message).toContain('Transmission');
  });

  it('rejects missing SABnzbd API key', async () => {
    downloadClientManagerMock.testConnection.mockResolvedValueOnce({
      success: false,
      message: 'API key is required for SABnzbd',
    });

    const { POST } = await import('@/app/api/setup/test-download-client/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({ type: 'sabnzbd', url: 'http://sab' }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/API key/);
  });

  it('tests SABnzbd connection', async () => {
    downloadClientManagerMock.testConnection.mockResolvedValueOnce({
      success: true,
      message: 'Successfully connected to SABnzbd (v3.0)',
    });

    const { POST } = await import('@/app/api/setup/test-download-client/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        type: 'sabnzbd',
        url: 'http://sab',
        password: 'api-key',
      }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.message).toContain('3.0');
  });

  it('returns error when SABnzbd connection fails', async () => {
    downloadClientManagerMock.testConnection.mockResolvedValueOnce({
      success: false,
      message: 'bad key',
    });

    const { POST } = await import('@/app/api/setup/test-download-client/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        type: 'sabnzbd',
        url: 'http://sab',
        password: 'api-key',
      }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/bad key/);
  });

  it('tests Prowlarr indexers', async () => {
    prowlarrMock.getIndexers.mockResolvedValue([
      { id: 1, name: 'Indexer', protocol: 'torrent', enable: true, capabilities: {} },
      { id: 2, name: 'Disabled', protocol: 'torrent', enable: false, capabilities: {} },
    ]);

    const { POST } = await import('@/app/api/setup/test-prowlarr/route');
    const response = await POST({ json: vi.fn().mockResolvedValue({ url: 'http://prowlarr', apiKey: 'key' }) } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.indexerCount).toBe(1);
  });

  it('validates OIDC issuer discovery', async () => {
    issuerMock.discover.mockResolvedValue({
      issuer: 'http://issuer',
      metadata: {
        authorization_endpoint: 'http://issuer/auth',
        token_endpoint: 'http://issuer/token',
        userinfo_endpoint: 'http://issuer/user',
        jwks_uri: 'http://issuer/jwks',
        scopes_supported: ['openid'],
        response_types_supported: ['code'],
      },
    });

    const { POST } = await import('@/app/api/setup/test-oidc/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        issuerUrl: 'http://issuer',
        clientId: 'client',
        clientSecret: 'secret',
      }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.issuer.authorizationEndpoint).toBe('http://issuer/auth');
  });

  it('returns error when OIDC fields are missing', async () => {
    const { POST } = await import('@/app/api/setup/test-oidc/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({ issuerUrl: 'http://issuer', clientId: 'client' }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/required/);
  });

  it('returns error when OIDC issuer URL is invalid', async () => {
    const { POST } = await import('@/app/api/setup/test-oidc/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        issuerUrl: 'not a url',
        clientId: 'client',
        clientSecret: 'secret',
      }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/Invalid issuer URL/);
  });

  it('returns error when OIDC issuer metadata is incomplete', async () => {
    issuerMock.discover.mockResolvedValue({
      issuer: 'http://issuer',
      metadata: {
        token_endpoint: 'http://issuer/token',
        userinfo_endpoint: 'http://issuer/user',
      },
    });

    const { POST } = await import('@/app/api/setup/test-oidc/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        issuerUrl: 'http://issuer',
        clientId: 'client',
        clientSecret: 'secret',
      }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(500);
    expect(payload.error).toMatch(/missing required endpoints/);
  });

  it('returns friendly error when OIDC discovery fails to resolve host', async () => {
    issuerMock.discover.mockRejectedValue(new Error('getaddrinfo ENOTFOUND issuer'));

    const { POST } = await import('@/app/api/setup/test-oidc/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        issuerUrl: 'http://issuer',
        clientId: 'client',
        clientSecret: 'secret',
      }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(500);
    expect(payload.error).toMatch(/Cannot reach OIDC provider/);
  });

  it('validates paths are writable', async () => {
    fsMock.access.mockRejectedValueOnce(new Error('missing'));
    fsMock.mkdir.mockResolvedValueOnce(undefined);
    fsMock.writeFile.mockResolvedValueOnce(undefined);
    fsMock.unlink.mockResolvedValueOnce(undefined);

    fsMock.access.mockResolvedValueOnce(undefined);
    fsMock.writeFile.mockResolvedValueOnce(undefined);
    fsMock.unlink.mockResolvedValueOnce(undefined);

    const { POST } = await import('@/app/api/setup/test-paths/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({ downloadDir: '/downloads', mediaDir: '/media' }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.downloadDir.valid).toBe(true);
  });

  it('validates path template when provided', async () => {
    fsMock.access.mockResolvedValue(undefined);
    fsMock.writeFile.mockResolvedValue(undefined);
    fsMock.unlink.mockResolvedValue(undefined);

    const { POST } = await import('@/app/api/setup/test-paths/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        downloadDir: '/downloads',
        mediaDir: '/media',
        audiobookPathTemplate: '{author}/{title} ({year})',
      }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.template).toBeDefined();
    expect(payload.template.isValid).toBe(true);
    expect(payload.template.previewPaths).toHaveLength(3);
  });

  it('returns error for invalid path template', async () => {
    fsMock.access.mockResolvedValue(undefined);
    fsMock.writeFile.mockResolvedValue(undefined);
    fsMock.unlink.mockResolvedValue(undefined);

    const { POST } = await import('@/app/api/setup/test-paths/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        downloadDir: '/downloads',
        mediaDir: '/media',
        audiobookPathTemplate: '{author}/{invalid_var}',
      }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.template).toBeDefined();
    expect(payload.template.isValid).toBe(false);
    expect(payload.template.error).toContain('No valid variable found in conditional block');
    expect(payload.template.previewPaths).toBeUndefined();
  });

  it('returns error when paths validation fails', async () => {
    fsMock.access.mockRejectedValue(new Error('missing'));
    fsMock.mkdir.mockRejectedValue(new Error('no permissions'));

    const { POST } = await import('@/app/api/setup/test-paths/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        downloadDir: '/bad/downloads',
        mediaDir: '/bad/media',
      }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(false);
    expect(payload.downloadDir.valid).toBe(false);
    expect(payload.mediaDir.valid).toBe(false);
    expect(payload.error).toBeDefined();
  });

  it('validates template with absolute path and returns error', async () => {
    fsMock.access.mockResolvedValue(undefined);
    fsMock.writeFile.mockResolvedValue(undefined);
    fsMock.unlink.mockResolvedValue(undefined);

    const { POST } = await import('@/app/api/setup/test-paths/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({
        downloadDir: '/downloads',
        mediaDir: '/media',
        audiobookPathTemplate: '/absolute/{author}/{title}',
      }),
    } as any);
    const payload = await response.json();

    expect(payload.template).toBeDefined();
    expect(payload.template.isValid).toBe(false);
    expect(payload.template.error).toContain('absolute');
  });

  it('tests Audiobookshelf connection with saved token', async () => {
    configServiceMock.get.mockResolvedValueOnce('token');
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: vi.fn().mockResolvedValue({ libraries: [{ id: '1', name: 'Lib', mediaType: 'book', stats: { totalItems: 10 } }] }),
    });
    vi.stubGlobal('fetch', fetchMock);

    const { POST } = await import('@/app/api/setup/test-abs/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({ serverUrl: 'http://abs', apiToken: '********' }),
    } as any);
    const payload = await response.json();

    expect(payload.success).toBe(true);
    expect(payload.libraries[0].id).toBe('1');
  });

  it('returns error when Audiobookshelf server URL is missing', async () => {
    const { POST } = await import('@/app/api/setup/test-abs/route');
    const response = await POST({ json: vi.fn().mockResolvedValue({}) } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/Server URL/);
  });

  it('returns error when saved Audiobookshelf token is missing', async () => {
    configServiceMock.get.mockResolvedValueOnce(null);

    const { POST } = await import('@/app/api/setup/test-abs/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({ serverUrl: 'http://abs', apiToken: '' }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/API token is required/);
  });

  it('returns error when Audiobookshelf connection fails', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      statusText: 'Unauthorized',
    });
    vi.stubGlobal('fetch', fetchMock);

    const { POST } = await import('@/app/api/setup/test-abs/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({ serverUrl: 'http://abs', apiToken: 'token' }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/Connection failed/);
  });

  it('returns error when Audiobookshelf response is missing libraries', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: vi.fn().mockResolvedValue({}),
    });
    vi.stubGlobal('fetch', fetchMock);

    const { POST } = await import('@/app/api/setup/test-abs/route');
    const response = await POST({
      json: vi.fn().mockResolvedValue({ serverUrl: 'http://abs', apiToken: 'token' }),
    } as any);
    const payload = await response.json();

    expect(response.status).toBe(400);
    expect(payload.error).toMatch(/Invalid response/);
  });
});


