import { Container } from 'inversify'
import { BusModule, Bus, BUS_SYMBOLS, ApplicationBootstrap } from '@node-ts/bus-core'
import { Persistence } from './persistence'
import { BUS_WORKFLOW_SYMBOLS } from '../bus-workflow-symbols'
import { TestCommand, TestWorkflowData, TestWorkflow, TaskRan, FinalTask } from '../test'
import { MessageWorkflowMapping } from './message-workflow-mapping'
import { sleep } from '../utility'
import { WorkflowStatus } from './workflow-data'
import { WorkflowRegistry } from './registry/workflow-registry'
import { BusWorkflowModule } from '../bus-workflow-module'
import { LoggerModule, LOGGER_SYMBOLS, Logger } from '@node-ts/logger-core'
import {
  TestWorkflowStartedByCompletes,
  TestWorkflowStartedByCompletesData
} from '../test/test-workflow-startedby-completes'
import {
  TestWorkflowStartedByDiscard,
  TestWorkflowStartedByDiscardData
} from '../test/test-workflow-startedby-discard'
import { Mock } from 'typemoq'
import { MessageAttributes } from '@node-ts/bus-messages'

describe('Workflow', () => {
  let container: Container
  let persistence: Persistence
  let bootstrap: ApplicationBootstrap

  const command = new TestCommand('abc')
  let bus: Bus
  const CONSUME_TIMEOUT = 500

  beforeAll(async () => {
    container = new Container()
    container.load(new LoggerModule())
    container.load(new BusModule())
    container.load(new BusWorkflowModule())
    container.rebind(LOGGER_SYMBOLS.Logger).toConstantValue(Mock.ofType<Logger>().object)

    persistence = container.get<Persistence>(BUS_WORKFLOW_SYMBOLS.Persistence)

    const workflowRegistry = container.get<WorkflowRegistry>(BUS_WORKFLOW_SYMBOLS.WorkflowRegistry)
    workflowRegistry.register(TestWorkflow, TestWorkflowData)
    workflowRegistry.register(TestWorkflowStartedByCompletes, TestWorkflowStartedByCompletesData)
    workflowRegistry.register(TestWorkflowStartedByDiscard, TestWorkflowStartedByDiscardData)
    await workflowRegistry.initializeWorkflows()

    bootstrap = container.get<ApplicationBootstrap>(BUS_SYMBOLS.ApplicationBootstrap)
    await bootstrap.initialize(container)

    bus = container.get(BUS_SYMBOLS.Bus)
    await bus.send(command)
    await sleep(CONSUME_TIMEOUT)
  })

  afterAll(async () => {
    await bootstrap.dispose()
  })

  describe('when a message that starts a workflow is received', () => {
    const propertyMapping = new MessageWorkflowMapping<TestCommand, TestWorkflowData> (
      cmd => cmd.property1,
      'property1'
    )
    let workflowData: TestWorkflowData[]
    const messageOptions = new MessageAttributes()

    beforeAll(async () => {
      workflowData = await persistence.getWorkflowData<TestWorkflowData, TestCommand>(
        TestWorkflowData,
        propertyMapping,
        command,
        messageOptions
      )
    })

    it('should start a new workflow', () => {
      expect(workflowData).toHaveLength(1)
      const data = workflowData[0]
      expect(data.$status).toEqual(WorkflowStatus.Running)
      expect(data.$version).toEqual(0)
      expect(data).toMatchObject({ property1: command.property1 })
    })

    describe('and then a message for the next step is received', () => {
      const event = new TaskRan('abc')
      let nextWorkflowData: TestWorkflowData[]

      beforeAll(async () => {
        await bus.publish(event)
        await sleep(CONSUME_TIMEOUT)

        nextWorkflowData = await persistence.getWorkflowData<TestWorkflowData, TestCommand>(
          TestWorkflowData,
          propertyMapping,
          command,
          messageOptions,
          true
        )
      })

      it('should handle that message', () => {
        expect(nextWorkflowData).toHaveLength(1)
      })

      describe('and then a final message arrives', () => {
        const finalTask = new FinalTask()
        let finalWorkflowData: TestWorkflowData[]

        beforeAll(async () => {
          await bus.publish(
            finalTask,
            new MessageAttributes({ correlationId: nextWorkflowData[0].$workflowId })
          )
          await sleep(CONSUME_TIMEOUT)

          finalWorkflowData = await persistence.getWorkflowData<TestWorkflowData, TestCommand>(
            TestWorkflowData,
            propertyMapping,
            command,
            messageOptions,
            true
          )
        })

        it('should mark the workflow as complete', () => {
          expect(finalWorkflowData).toHaveLength(1)
          const data = finalWorkflowData[0]
          expect(data.$status).toEqual(WorkflowStatus.Complete)
        })
      })
    })
  })

  describe('when a workflow is completed in a StartedBy handler', () => {
    const messageOptions = new MessageAttributes()
    const propertyMapping = new MessageWorkflowMapping<TestCommand, TestWorkflowStartedByCompletesData> (
      cmd => cmd.property1,
      'property1'
    )

    it('should persist the workflow as completed', async () => {
      const workflowData = await persistence.getWorkflowData<TestWorkflowStartedByCompletesData, TestCommand>(
        TestWorkflowStartedByCompletesData,
        propertyMapping,
        command,
        messageOptions,
        true
      )

      expect(workflowData).toHaveLength(1)
      const data = workflowData[0]
      expect(data.$status).toEqual(WorkflowStatus.Complete)
    })
  })

  describe('when a StartedBy handler returns a discardStep', () => {
    const messageOptions = new MessageAttributes()
    const propertyMapping = new MessageWorkflowMapping<TestCommand, TestWorkflowStartedByDiscardData> (
      cmd => cmd.property1,
      'property1'
    )

    it('should not persist the workflow', async () => {
      const workflowData = await persistence.getWorkflowData<TestWorkflowStartedByDiscardData, TestCommand>(
        TestWorkflowStartedByDiscardData,
        propertyMapping,
        command,
        messageOptions,
        true
      )

      expect(workflowData).toHaveLength(0)
    })
  })
})
