import fs from 'fs';
import path from 'path';

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

import { state } from './state.js';
import { detectPackageManager, getConfig, getConnection, http, upgradeAction } from './utils.js';
import { NANGO_VERSION } from './version.js';

describe('detectPackageManager', () => {
    afterEach(() => {
        vi.restoreAllMocks();
    });

    function mockFs(dirs: Record<string, { files?: string[]; packageManager?: string }>) {
        vi.spyOn(fs, 'readdirSync').mockImplementation((dir) => {
            return (dirs[dir as string]?.files ?? []) as any;
        });
        vi.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
            const dir = path.dirname(filePath as string);
            const pm = dirs[dir]?.packageManager;
            return JSON.stringify(pm ? { packageManager: pm } : {});
        });
    }

    it('detects pnpm via lock file', () => {
        mockFs({ '/project': { files: ['pnpm-lock.yaml'] } });
        expect(detectPackageManager({ fullPath: '/project' })).toBe('pnpm');
    });

    it('detects yarn via lock file', () => {
        mockFs({ '/project': { files: ['yarn.lock'] } });
        expect(detectPackageManager({ fullPath: '/project' })).toBe('yarn');
    });

    it('detects bun via bun.lockb', () => {
        mockFs({ '/project': { files: ['bun.lockb'] } });
        expect(detectPackageManager({ fullPath: '/project' })).toBe('bun');
    });

    it('detects bun via bun.lock', () => {
        mockFs({ '/project': { files: ['bun.lock'] } });
        expect(detectPackageManager({ fullPath: '/project' })).toBe('bun');
    });

    it('falls back to npm when no signals found', () => {
        mockFs({ '/project': { files: [] } });
        expect(detectPackageManager({ fullPath: '/project' })).toBe('npm');
    });

    it('detects package manager via packageManager field in package.json', () => {
        mockFs({ '/project': { files: ['package.json'], packageManager: 'pnpm@9.0.0' } });
        expect(detectPackageManager({ fullPath: '/project' })).toBe('pnpm');
    });

    it('packageManager field takes priority over lock files', () => {
        mockFs({ '/project': { files: ['package.json', 'pnpm-lock.yaml'], packageManager: 'yarn@4.1.0' } });
        expect(detectPackageManager({ fullPath: '/project' })).toBe('yarn');
    });

    it('detects pnpm lock file in a parent directory (monorepo)', () => {
        mockFs({
            '/project/apps/integrations': { files: [] },
            '/project/apps': { files: [] },
            '/project': { files: ['pnpm-lock.yaml'] }
        });
        expect(detectPackageManager({ fullPath: '/project/apps/integrations' })).toBe('pnpm');
    });

    it('detects packageManager field in a parent package.json (monorepo)', () => {
        mockFs({
            '/project/apps/integrations': { files: [] },
            '/project/apps': { files: [] },
            '/project': { files: ['package.json'], packageManager: 'pnpm@9.0.0' }
        });
        expect(detectPackageManager({ fullPath: '/project/apps/integrations' })).toBe('pnpm');
    });

    it('nearest package.json wins over parent lock file', () => {
        mockFs({
            '/project/apps/integrations': { files: ['package.json'], packageManager: 'yarn@4.1.0' },
            '/project/apps': { files: [] },
            '/project': { files: ['pnpm-lock.yaml'] }
        });
        expect(detectPackageManager({ fullPath: '/project/apps/integrations' })).toBe('yarn');
    });
});

describe('resolveHostport', () => {
    beforeEach(() => {
        vi.resetModules();
    });

    afterEach(() => {
        vi.unstubAllEnvs();
    });

    async function importResolveHostport() {
        const mod = await import('./utils.js');
        return mod.resolveHostport;
    }

    it('defaults to cloud host when NANGO_HOSTPORT is not set', async () => {
        vi.stubEnv('NANGO_HOSTPORT', '');
        const resolveHostport = await importResolveHostport();
        expect(resolveHostport()).toBe('https://api.nango.dev');
    });

    it('uses NANGO_HOSTPORT when set', async () => {
        vi.stubEnv('NANGO_HOSTPORT', 'http://localhost:3003');
        const resolveHostport = await importResolveHostport();
        expect(resolveHostport()).toBe('http://localhost:3003');
    });

    it('strips trailing slash from NANGO_HOSTPORT', async () => {
        vi.stubEnv('NANGO_HOSTPORT', 'http://localhost:3003/');
        const resolveHostport = await importResolveHostport();
        expect(resolveHostport()).toBe('http://localhost:3003');
    });

    it('returns localhostUrl for env=local when NANGO_HOSTPORT is not set', async () => {
        vi.stubEnv('NANGO_HOSTPORT', '');
        const resolveHostport = await importResolveHostport();
        expect(resolveHostport('local')).toBe('http://localhost:3003');
    });

    it('ignores env=local when NANGO_HOSTPORT is explicitly set', async () => {
        vi.stubEnv('NANGO_HOSTPORT', 'https://my-nango.example.com');
        const resolveHostport = await importResolveHostport();
        expect(resolveHostport('local')).toBe('https://my-nango.example.com');
    });
});

describe('getEnvironments', () => {
    afterEach(() => {
        vi.unstubAllEnvs();
        vi.restoreAllMocks();
    });

    it('warns and returns undefined when NANGO_SECRET_KEY is not set', async () => {
        vi.stubEnv('NANGO_SECRET_KEY', '');
        const { getEnvironments } = await import('./utils.js');
        const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
        const result = await getEnvironments();
        expect(result).toBeUndefined();
        expect(warn).toHaveBeenCalledOnce();
    });
});

describe('connection helpers', () => {
    afterEach(() => {
        vi.restoreAllMocks();
    });

    it('returns a Result success for getConnection', async () => {
        const data = { data: { id: 1, connection_id: 'conn-1' } };
        const get = vi.spyOn(http, 'get').mockResolvedValue({ data } as any);

        const result = await getConnection('github', 'conn-1', { 'Nango-Is-Dry-Run': true });

        expect(result.isOk()).toBe(true);
        if (result.isOk()) {
            expect(result.value).toStrictEqual(data);
        }
        expect(get).toHaveBeenCalledWith(expect.stringContaining('/connection/conn-1'), {
            params: { provider_config_key: 'github' },
            headers: expect.objectContaining({
                'Nango-Is-Dry-Run': true,
                Authorization: expect.any(String)
            })
        });
    });

    it('returns a Result error for getConnection request failures', async () => {
        vi.spyOn(http, 'get').mockRejectedValue(new Error('connection lookup failed'));

        const result = await getConnection('github', 'conn-1');

        expect(result.isErr()).toBe(true);
        if (result.isErr()) {
            expect(result.error.message).toContain('connection lookup failed');
        }
    });

    it('returns a Result success for getConfig', async () => {
        const data = { data: { provider: 'github' } };
        const get = vi.spyOn(http, 'get').mockResolvedValue({ data } as any);

        const result = await getConfig('github');

        expect(result.isOk()).toBe(true);
        if (result.isOk()) {
            expect(result.value).toStrictEqual(data);
        }
        expect(get).toHaveBeenCalledWith(expect.stringContaining('/integrations/github'), {
            headers: expect.objectContaining({
                Authorization: expect.any(String)
            })
        });
    });

    it('returns a Result error for getConfig request failures', async () => {
        vi.spyOn(http, 'get').mockRejectedValue(new Error('config lookup failed'));

        const result = await getConfig('github');

        expect(result.isErr()).toBe(true);
        if (result.isErr()) {
            expect(result.error.message).toContain('config lookup failed');
        }
    });
});

describe('upgradeAction', () => {
    afterEach(() => {
        vi.unstubAllEnvs();
        vi.restoreAllMocks();
    });

    it('checks the npm registry without the Nango API client', async () => {
        vi.stubEnv('NANGO_CLI_UPGRADE_MODE', 'auto');
        vi.spyOn(state, 'get').mockReturnValue(undefined);
        const nangoHttpGet = vi.spyOn(http, 'get').mockRejectedValue(new Error('Nango API client must not be used for the npm registry'));
        const registryGet = vi.spyOn(axios, 'get').mockResolvedValue({ data: { 'dist-tags': { latest: NANGO_VERSION } } });

        await upgradeAction();

        expect(registryGet).toHaveBeenCalledWith(expect.stringContaining('registry.npmjs.org/nango'));
        expect(nangoHttpGet).not.toHaveBeenCalled();
    });
});