import * as StackbitTypes from '@stackbit/types';
import * as stackbitUtils from '@stackbit/types';
import type { GitFileStatus, InitOptions, Logger } from '@stackbit/types';

function typedMock<T extends ((...args: any) => any) | undefined>(func: T) {
    return jest.fn<T extends (...args: any) => any ? ReturnType<T> : any, T extends (...args: any) => any ? Parameters<T> : any>(func);
}

export function createMockedContentSource(
    options: {
        srcType?: string;
        srcProjectId?: string;
    } & Partial<StackbitTypes.ContentSourceInterface> = {}
): jest.Mocked<StackbitTypes.ContentSourceInterface> {
    return {
        getVersion: jest.fn(() => stackbitUtils.getVersion({ contentSourceVersion: 'x.y.z' })),
        getContentSourceType: jest.fn(() => options.srcType ?? 'src-type-1'),
        getProjectId: jest.fn(() => options.srcProjectId ?? 'src-id-1'),
        getProjectEnvironment: jest.fn(() => 'production'),
        getProjectManageUrl: jest.fn(() => 'https://www.example.com'),
        init: jest.fn(options.init ?? (async () => {})),
        reset: jest.fn(options.reset ?? (async () => {})),
        destroy: jest.fn(options.destroy ?? (async () => {})),
        onFilesChange: jest.fn(
            options.onFilesChange ??
                (async () => {
                    return {
                        invalidateSchema: false,
                        contentChanges: {}
                    };
                })
        ),
        startWatchingContentUpdates: jest.fn(options.startWatchingContentUpdates ?? ((): void => {})),
        stopWatchingContentUpdates: jest.fn(options.stopWatchingContentUpdates ?? ((): void => {})),
        getSchema: jest.fn(
            options.getSchema ??
                (async () => {
                    return {
                        models: [],
                        locales: [],
                        context: null
                    };
                })
        ),
        getDocuments: jest.fn(options.getDocuments ?? (async () => [])),
        getAssets: jest.fn(options.getAssets ?? (async () => [])),
        hasAccess: jest.fn(options.hasAccess ?? (async (options) => ({ hasConnection: true, hasPermissions: true }))),
        createDocument: jest.fn(
            options.createDocument ??
                (async () => {
                    throw new Error('Function not implemented.');
                })
        ),
        updateDocument: jest.fn(
            options.updateDocument ??
                (async () => {
                    throw new Error('Function not implemented.');
                })
        ),
        deleteDocument: jest.fn(
            options.deleteDocument ??
                (async () => {
                    throw new Error('Function not implemented.');
                })
        ),
        uploadAsset: jest.fn(
            options.uploadAsset ??
                (async () => {
                    throw new Error('Function not implemented.');
                })
        ),
        validateDocuments: jest.fn(
            options.validateDocuments ??
                (async () => {
                    throw new Error('Function not implemented.');
                })
        ),
        publishDocuments: jest.fn(
            options.publishDocuments ??
                (async () => {
                    throw new Error('Function not implemented.');
                })
        ),
        getDocumentVersions: options.getDocumentVersions ? jest.fn(options.getDocumentVersions) : undefined,
        getDocumentForVersion: options.getDocumentForVersion ? jest.fn(options.getDocumentForVersion) : undefined
    };
}

export function createMockInitOptions(options: Partial<InitOptions> = {}) {
    return jest.mocked({
        logger: createMockedLogger(),
        userLogger: createMockedLogger(),
        localDev: options.localDev ?? true,
        stackbitConfigFilePath: 'stackbit.config.ts',
        runCommand: createMockedCommandRunner(),
        git: createMockedGitService(),
        cache: createMockedCache(options.cache)
    });
}

export function createMockedLogger(): jest.Mocked<Logger> {
    const mockedLogger: jest.Mocked<Logger> = {
        createLogger: jest.fn(({ label }: { label: string }): Logger => {
            return mockedLogger;
        }),
        error: jest.fn((message: string) => {}),
        warn: jest.fn((message: string) => {}),
        info: jest.fn((message: string) => {}),
        debug: jest.fn((message: string) => {})
    };
    return mockedLogger;
}

export function createMockedCache<SchemaContext = unknown, DocumentContext = unknown, AssetContext = unknown, ModelContext = unknown>(
    partialCacheMock: Partial<StackbitTypes.Cache<SchemaContext, DocumentContext, AssetContext, ModelContext>> = {}
): jest.Mocked<StackbitTypes.Cache<SchemaContext, DocumentContext, AssetContext, ModelContext>> {
    return {
        getDocuments: jest.fn(partialCacheMock.getDocuments ?? (() => [])),
        getAssets: jest.fn(partialCacheMock.getAssets ?? (() => [])),
        getSchema: jest.fn(partialCacheMock.getSchema ?? (() => ({ models: [], locales: [], context: {} as SchemaContext }))),
        getScheduledActions: jest.fn(partialCacheMock.getScheduledActions ?? (() => [])),
        getDocumentById: jest.fn(partialCacheMock.getDocumentById ?? (() => undefined)),
        getAssetById: jest.fn(partialCacheMock.getAssetById ?? (() => undefined)),
        getModelByName: jest.fn(partialCacheMock.getModelByName ?? (() => undefined)),
        getScheduledActionsForDocumentId: jest.fn(partialCacheMock.getScheduledActionsForDocumentId ?? (() => [])),
        updateContent: jest.fn(partialCacheMock.updateContent ?? (async () => undefined)),
        getSyncContext: jest.fn(partialCacheMock.getSyncContext ?? (() => ({}))),
        invalidateSchema: jest.fn(partialCacheMock.invalidateSchema ?? (async () => undefined)),
        clearSyncContext: jest.fn(partialCacheMock.clearSyncContext ?? (async () => undefined)),
        requestSync: jest.fn(partialCacheMock.requestSync ?? (async () => undefined)),
        set: jest.fn(partialCacheMock.set ?? (async () => undefined)),
        get: jest.fn(partialCacheMock.get ?? (async () => undefined)),
        remove: jest.fn(partialCacheMock.remove ?? (async () => undefined))
    };
}

export function createMockedGitService(): jest.Mocked<StackbitTypes.GitServiceInterface> {
    return {
        getRepoUrl: jest.fn(() => ''),
        getRepoBranch: jest.fn(() => ''),
        getRepoPublishBranch: jest.fn(() => ''),
        getRepoDir: jest.fn(() => ''),
        setRepoUrl: jest.fn(),
        setRepoBranch: jest.fn(),
        setRepoPublishBranch: jest.fn(),
        commitAndPush: typedMock<StackbitTypes.GitServiceInterface['commitAndPush']>(async () => undefined),
        publish: typedMock<StackbitTypes.GitServiceInterface['publish']>(async () => undefined),
        commitLog: jest.fn(async () => []),
        diff: jest.fn(async () => []),
        addPullListener: typedMock<StackbitTypes.GitServiceInterface['addPullListener']>(() => undefined),
        removePullListener: typedMock<StackbitTypes.GitServiceInterface['removePullListener']>(() => undefined),
        addPushListener: typedMock<StackbitTypes.GitServiceInterface['addPushListener']>(() => undefined),
        removePushListener: typedMock<StackbitTypes.GitServiceInterface['removePushListener']>(() => undefined),
        diffFilesWithFetchHead: jest.fn(async () => []),
        pullFilesFromFetchHead: jest.fn(async (files: GitFileStatus[]) => {}),
        createShadowRepo: jest.fn(async () => {})
    };
}

export function createMockedCommandRunner(): jest.MockedFunction<StackbitTypes.CommandRunner> {
    return typedMock<StackbitTypes.CommandRunner>(async () => {
        return { stdout: '', stderr: '' };
    });
}
