import {
    FlowOperationType,
    FlowStatus,
    FlowVersionState,
    PrincipalType,
} from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
    createMockFlow,
    createMockFlowVersion,
    mockAndSaveBasicSetup,
} from '../../../helpers/mocks'

let app: FastifyInstance | null = null

beforeAll(async () => {
    await initializeDatabase({ runMigrations: false })
    app = await setupServer()
})

afterAll(async () => {
    await databaseConnection().destroy()
    await app?.close()
})

describe('Flow API', () => {
    describe('Create Flow endpoint', () => {
        it('Adds an empty flow', async () => {
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
            const mockToken = await generateMockToken({
                type: PrincipalType.USER,
                projectId: mockProject.id,
                id: mockOwner.id,
            })

            const mockCreateFlowRequest = {
                displayName: 'test flow',
                projectId: mockProject.id,
                metadata: {
                    foo: 'bar',
                },
            }

            // act
            const response = await app?.inject({
                method: 'POST',
                url: '/v1/flows',
                query: {
                    projectId: mockProject.id,
                },
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
                body: mockCreateFlowRequest,
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.CREATED)
            const responseBody = response?.json()

            expect(Object.keys(responseBody)).toHaveLength(12)
            expect(responseBody?.id).toHaveLength(21)
            expect(responseBody?.created).toBeDefined()
            expect(responseBody?.updated).toBeDefined()
            expect(responseBody?.projectId).toBe(mockProject.id)
            expect(responseBody?.folderId).toBeNull()
            expect(responseBody?.status).toBe('DISABLED')
            expect(responseBody?.publishedVersionId).toBeNull()
            expect(responseBody?.schedule).toBeNull()
            expect(responseBody?.metadata).toMatchObject({ foo: 'bar' })
            expect(responseBody?.handshakeConfiguration).toBeNull()

            expect(Object.keys(responseBody?.version)).toHaveLength(11)
            expect(responseBody?.version?.id).toHaveLength(21)
            expect(responseBody?.version?.created).toBeDefined()
            expect(responseBody?.version?.updated).toBeDefined()
            expect(responseBody?.version?.updatedBy).toBeNull()
            expect(responseBody?.version?.flowId).toBe(responseBody?.id)
            expect(responseBody?.version?.displayName).toBe('test flow')
            expect(Object.keys(responseBody?.version?.trigger)).toHaveLength(5)
            expect(responseBody?.version?.trigger.type).toBe('EMPTY')
            expect(responseBody?.version?.trigger.name).toBe('trigger')
            expect(responseBody?.version?.trigger.settings).toMatchObject({})
            expect(responseBody?.version?.trigger.valid).toBe(false)
            expect(responseBody?.version?.trigger.displayName).toBe('Select Trigger')
            expect(responseBody?.version?.valid).toBe(false)
            expect(responseBody?.version?.state).toBe('DRAFT')
        })
    })

    describe('Update status endpoint', () => {
        it('Enables a disabled Flow', async () => {
            // arrange
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()

            const mockFlow = createMockFlow({
                projectId: mockProject.id,
                status: FlowStatus.DISABLED,
            })
            await databaseConnection().getRepository('flow').save([mockFlow])

            const mockFlowVersion = createMockFlowVersion({
                flowId: mockFlow.id,
                updatedBy: mockOwner.id,
            })
            await databaseConnection()
                .getRepository('flow_version')
                .save([mockFlowVersion])

            await databaseConnection().getRepository('flow').update(mockFlow.id, {
                publishedVersionId: mockFlowVersion.id,
            })

            const mockToken = await generateMockToken({
                type: PrincipalType.USER,
                projectId: mockProject.id,
                id: mockOwner.id,
            })

            const mockUpdateFlowStatusRequest = {
                type: FlowOperationType.CHANGE_STATUS,
                request: {
                    status: 'ENABLED',
                },
            }

            // act
            const response = await app?.inject({
                method: 'POST',
                url: `/v1/flows/${mockFlow.id}`,
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
                body: mockUpdateFlowStatusRequest,
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.OK)
            const responseBody = response?.json()

            expect(Object.keys(responseBody)).toHaveLength(12)
            expect(responseBody?.id).toBe(mockFlow.id)
            expect(responseBody?.created).toBeDefined()
            expect(responseBody?.updated).toBeDefined()
            expect(responseBody?.projectId).toBe(mockProject.id)
            expect(responseBody?.folderId).toBeNull()
            expect(responseBody?.status).toBe('ENABLED')
            expect(responseBody?.publishedVersionId).toBe(mockFlowVersion.id)
            expect(responseBody?.schedule).toBeNull()
            expect(responseBody?.metadata).toBeNull()
            expect(responseBody?.handshakeConfiguration).toBeNull()

            expect(Object.keys(responseBody?.version)).toHaveLength(11)
            expect(responseBody?.version?.id).toBe(mockFlowVersion.id)
        })

        it('Disables an enabled Flow', async () => {
            // arrange
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()

            const mockFlow = createMockFlow({
                projectId: mockProject.id,
                status: FlowStatus.ENABLED,
            })
            await databaseConnection().getRepository('flow').save([mockFlow])

            const mockFlowVersion = createMockFlowVersion({
                flowId: mockFlow.id,
                updatedBy: mockOwner.id,
            })
            await databaseConnection()
                .getRepository('flow_version')
                .save([mockFlowVersion])

            await databaseConnection().getRepository('flow').update(mockFlow.id, {
                publishedVersionId: mockFlowVersion.id,
            })

            const mockToken = await generateMockToken({
                type: PrincipalType.USER,
                projectId: mockProject.id,
                id: mockOwner.id,
            })

            const mockUpdateFlowStatusRequest = {
                type: FlowOperationType.CHANGE_STATUS,
                request: {
                    status: 'DISABLED',
                },
            }

            // act
            const response = await app?.inject({
                method: 'POST',
                url: `/v1/flows/${mockFlow.id}`,
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
                body: mockUpdateFlowStatusRequest,
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.OK)
            const responseBody = response?.json()

            expect(Object.keys(responseBody)).toHaveLength(12)
            expect(responseBody?.id).toBe(mockFlow.id)
            expect(responseBody?.created).toBeDefined()
            expect(responseBody?.updated).toBeDefined()
            expect(responseBody?.projectId).toBe(mockProject.id)
            expect(responseBody?.folderId).toBeNull()
            expect(responseBody?.status).toBe('DISABLED')
            expect(responseBody?.publishedVersionId).toBe(mockFlowVersion.id)
            expect(responseBody?.schedule).toBeNull()
            expect(responseBody?.metadata).toBeNull()
            expect(responseBody?.handshakeConfiguration).toBeNull()

            expect(Object.keys(responseBody?.version)).toHaveLength(11)
            expect(responseBody?.version?.id).toBe(mockFlowVersion.id)
        })
    })

    describe('Update published version id endpoint', () => {
        it('Publishes latest draft version', async () => {
            // arrange
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()

            const mockFlow = createMockFlow({
                projectId: mockProject.id,
                status: FlowStatus.DISABLED,
            })
            await databaseConnection().getRepository('flow').save([mockFlow])

            const mockFlowVersion = createMockFlowVersion({
                flowId: mockFlow.id,
                updatedBy: mockOwner.id,
                state: FlowVersionState.DRAFT,
            })
            await databaseConnection()
                .getRepository('flow_version')
                .save([mockFlowVersion])

            const mockToken = await generateMockToken({
                id: mockOwner.id,
                type: PrincipalType.USER,
                projectId: mockProject.id,
            })

            // act
            const response = await app?.inject({
                method: 'POST',
                url: `/v1/flows/${mockFlow.id}`,
                body: {
                    type: FlowOperationType.LOCK_AND_PUBLISH,
                    request: {},
                },
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.OK)
            const responseBody = response?.json()

            expect(Object.keys(responseBody)).toHaveLength(12)
            expect(responseBody?.id).toBe(mockFlow.id)
            expect(responseBody?.created).toBeDefined()
            expect(responseBody?.updated).toBeDefined()
            expect(responseBody?.projectId).toBe(mockProject.id)
            expect(responseBody?.folderId).toBeNull()
            expect(responseBody?.status).toBe('ENABLED')
            expect(responseBody?.publishedVersionId).toBe(mockFlowVersion.id)
            expect(responseBody?.schedule).toBeNull()
            expect(responseBody?.metadata).toBeNull()
            expect(responseBody?.handshakeConfiguration).toBeNull()

            expect(Object.keys(responseBody?.version)).toHaveLength(11)
            expect(responseBody?.version?.id).toBe(mockFlowVersion.id)
            expect(responseBody?.version?.state).toBe('LOCKED')
        })
    })

    describe('List Flows endpoint', () => {
        it('Filters Flows by status', async () => {
            // arrange
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()

            const mockEnabledFlow = createMockFlow({
                projectId: mockProject.id,
                status: FlowStatus.ENABLED,
            })
            const mockDisabledFlow = createMockFlow({
                projectId: mockProject.id,
                status: FlowStatus.DISABLED,
            })
            await databaseConnection()
                .getRepository('flow')
                .save([mockEnabledFlow, mockDisabledFlow])

            const mockEnabledFlowVersion = createMockFlowVersion({
                flowId: mockEnabledFlow.id,
            })
            const mockDisabledFlowVersion = createMockFlowVersion({
                flowId: mockDisabledFlow.id,
            })
            await databaseConnection()
                .getRepository('flow_version')
                .save([mockEnabledFlowVersion, mockDisabledFlowVersion])

            const mockToken = await generateMockToken({
                type: PrincipalType.USER,
                projectId: mockProject.id,
                id: mockOwner.id,
            })

            // act
            const response = await app?.inject({
                method: 'GET',
                url: '/v1/flows',
                query: {
                    projectId: mockProject.id,
                    status: 'ENABLED',
                },
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.OK)
            const responseBody = response?.json()

            expect(responseBody.data).toHaveLength(1)
            expect(responseBody.data[0].id).toBe(mockEnabledFlow.id)
        })

        it('Populates Flow version', async () => {
            // arrange
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()

            const mockFlow = createMockFlow({ projectId: mockProject.id })
            await databaseConnection().getRepository('flow').save([mockFlow])

            const mockFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
            await databaseConnection()
                .getRepository('flow_version')
                .save([mockFlowVersion])

            const mockToken = await generateMockToken({
                type: PrincipalType.USER,
                projectId: mockProject.id,
                id: mockOwner.id,
            })

            // act
            const response = await app?.inject({
                method: 'GET',
                url: '/v1/flows',
                query: {
                    projectId: mockProject.id,
                },
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.OK)
            const responseBody = response?.json()

            expect(responseBody?.data).toHaveLength(1)
            expect(responseBody?.data?.[0]?.id).toBe(mockFlow.id)
            expect(responseBody?.data?.[0]?.version?.id).toBe(mockFlowVersion.id)
        })

        it('Fails if a flow with no version exists', async () => {
            // arrange
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
            
            const mockFlow = createMockFlow({ projectId: mockProject.id })
            await databaseConnection().getRepository('flow').save([mockFlow])

            const mockToken = await generateMockToken({
                type: PrincipalType.USER,
                projectId: mockProject.id,
                id: mockOwner.id,
            })

            // act
            const response = await app?.inject({
                method: 'GET',
                url: '/v1/flows',
                query: {
                    projectId: mockProject.id,
                },
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
            const responseBody = response?.json()

            expect(responseBody?.code).toBe('ENTITY_NOT_FOUND')
            expect(responseBody?.params?.entityType).toBe('FlowVersion')
            expect(responseBody?.params?.message).toBe(`flowId=${mockFlow.id}`)
        })
    })

    describe('Update Metadata endpoint', () => {
        it('Updates flow metadata', async () => {
            // arrange
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()

            // create a flow with no metadata
            const mockFlow = createMockFlow({ projectId: mockProject.id })
            await databaseConnection().getRepository('flow').save([mockFlow])

            const mockFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
            await databaseConnection()
                .getRepository('flow_version')
                .save([mockFlowVersion])

            const mockToken = await generateMockToken({
                type: PrincipalType.USER,
                projectId: mockProject.id,
                id: mockOwner.id,
            })

            const updatedMetadata = { foo: 'bar' }

            // act
            const response = await app?.inject({
                method: 'POST',
                url: `/v1/flows/${mockFlow.id}`,
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
                body: {
                    type: FlowOperationType.UPDATE_METADATA,
                    request: {
                        metadata: updatedMetadata,
                    },
                },
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.OK)
            const responseBody = response?.json()

            expect(responseBody.id).toBe(mockFlow.id)
            expect(responseBody.metadata).toEqual(updatedMetadata)

            // Verify metadata was actually persisted in the database
            const updatedFlow = await databaseConnection()
                .getRepository('flow')
                .findOneBy({ id: mockFlow.id })

            expect(updatedFlow?.metadata).toEqual(updatedMetadata)
        })
    })

    describe('Export Flow Template endpoint', () => {
        it('Exports a flow template using an API key', async () => {
            // arrange
            const { mockProject, mockOwner } = await mockAndSaveBasicSetup()

            const mockFlow = createMockFlow({
                projectId: mockProject.id,
                status: FlowStatus.ENABLED,
            })
            await databaseConnection().getRepository('flow').save([mockFlow])

            const mockFlowVersion = createMockFlowVersion({
                flowId: mockFlow.id,
                updatedBy: mockOwner.id,
            })
            await databaseConnection()
                .getRepository('flow_version')
                .save([mockFlowVersion])

            const mockApiKey = 'test_api_key'
            const mockToken = await generateMockToken({
                type: PrincipalType.SERVICE,
                projectId: mockProject.id,
                id: mockApiKey,
            })

            // act
            const response = await app?.inject({
                method: 'GET',
                url: `/v1/flows/${mockFlow.id}/template`,
                headers: {
                    authorization: `Bearer ${mockToken}`,
                },
            })

            // assert
            expect(response?.statusCode).toBe(StatusCodes.OK)
            const responseBody = response?.json()

            expect(responseBody).toHaveProperty('name')
            expect(responseBody).toHaveProperty('description')
            expect(responseBody).toHaveProperty('template')
            expect(responseBody.template).toHaveProperty('trigger')
        })
    })
})
