import { ISentRequest } from '../../models/SentRequest.js'
import { IResponse } from '../../models/Response.js'
import { IErrorResponse } from '../../models/ErrorResponse.js'
import { AssertionResult, IHttpAssertion } from '../../models/http-flows/HttpAssertion.js'
import { IHttpStep } from '../../models/http-flows/HttpStep.js'
import { FlowRequestDataEnum, FlowResponseDataEnum, FlowSourceEnum } from '../../models/http-flows/HttpFlows.js'
import { IReadDataSourceStep, ReadDataSourceStepKind } from '../../models/http-flows/steps/data/ReadDataSourceStep.js'
import { IReadValueStep, ReadValueStepKind } from '../../models/http-flows/steps/data/ReadValueStep.js'
import { AsLowerCaseStepKind } from '../../models/http-flows/steps/transformation/AsLowerCaseStep.js'
import { AsUpperCaseStepKind } from '../../models/http-flows/steps/transformation/AsUpperCaseStep.js'
import { AsNumberStepKind } from '../../models/http-flows/steps/transformation/AsNumberStep.js'
import { RoundStepKind } from '../../models/http-flows/steps/transformation/RoundStep.js'
import { SubstringStepKind } from '../../models/http-flows/steps/transformation/SubstringStep.js'
import { TrimStepKind } from '../../models/http-flows/steps/transformation/TrimStep.js'
import { EqualAssertionKind, IEqualAssertion } from '../../models/http-flows/steps/assertion/EqualAssertion.js'
import { INotEqualAssertion, NotEqualAssertionKind } from '../../models/http-flows/steps/assertion/NotEqualAssertion.js'
import { AssertionError } from '../../models/http-flows/AssertionError.js'
import {
  GreaterThanAssertionKind,
  IGreaterThanAssertion,
} from '../../models/http-flows/steps/assertion/GreaterThanAssertion.js'
import { ILessThanAssertion, LessThanAssertionKind } from '../../models/http-flows/steps/assertion/LessThanAssertion.js'
import { IIncludesAssertion, IncludesAssertionKind } from '../../models/http-flows/steps/assertion/IncludesAssertion.js'
import {
  INotIncludesAssertion,
  NotIncludesAssertionKind,
} from '../../models/http-flows/steps/assertion/NotIncludesAssertion.js'
import { OkAssertionKind } from '../../models/http-flows/steps/assertion/OkAssertion.js'
import { NotOkAssertionKind } from '../../models/http-flows/steps/assertion/NotOkAssertion.js'
import { HttpFlows, IStepValue } from './HttpFlows.js'

/**
 * A class that perform assertions that have been defined on an HTTP request.
 */
export class HttpAssertionRunner extends HttpFlows {
  report: AssertionResult[] = []

  /**
   *
   * @param request The request object sent to the server
   * @param response The generated response
   * @param assertions The list of assertions to perform.
   */
  async run(
    request: ISentRequest,
    response: IResponse | IErrorResponse,
    assertions: IHttpAssertion[]
  ): Promise<AssertionResult[]> {
    this.report = []
    if (!assertions || !assertions.length) {
      return [...this.report]
    }

    for (const assertion of assertions) {
      await this.runAssertion(request, response, assertion)
    }
    return [...this.report]
  }

  protected async runAssertion(
    request: ISentRequest,
    response: IResponse | IErrorResponse,
    assertion: IHttpAssertion
  ): Promise<void> {
    const { steps } = assertion

    if (!Array.isArray(steps) || !steps.length) {
      return
    }

    // this keeps a value from the last step to be passed to the next step.
    let lastResult: IStepValue = {
      value: undefined,
    }
    let error: AssertionError | undefined
    for (const step of steps) {
      try {
        lastResult = await this.runStep(request, step, lastResult, response)
      } catch (e) {
        const typed = e as AssertionError
        if (typed.name === 'AssertionError') {
          error = typed
        } else {
          const err = e as Error
          error = new AssertionError({
            message: err.message,
          })
        }
        break
      }
    }
    const description = assertion.description || this.createDescription(steps)
    if (error) {
      this.report.push({
        ok: false,
        description,
        error: error.toJSON(),
      })
    } else {
      this.report.push({
        ok: true,
        description,
      })
    }
  }

  protected createDescription(steps: IHttpStep[]): string {
    const parts: string[] = []
    for (const step of steps) {
      switch (step.kind) {
        case ReadDataSourceStepKind:
          parts.push(this.getDataSourceStepLabel(step as IReadDataSourceStep))
          break
        case ReadValueStepKind:
          parts.push(this.getValueStepLabel(step as IReadValueStep))
          break
        case AsLowerCaseStepKind:
          parts.push('To lower case')
          break
        case AsUpperCaseStepKind:
          parts.push('To upper case')
          break
        case AsNumberStepKind:
          parts.push('As number')
          break
        case RoundStepKind:
          parts.push('Round')
          break
        case TrimStepKind:
          parts.push('Trim')
          break
        case SubstringStepKind:
          parts.push('Substring')
          break
        case EqualAssertionKind:
          parts.push(`Equals to ${(step as IEqualAssertion).value}`)
          break
        case NotEqualAssertionKind:
          parts.push(`Not equals to ${(step as INotEqualAssertion).value}`)
          break
        case GreaterThanAssertionKind:
          parts.push(`Is greater than ${(step as IGreaterThanAssertion).value}`)
          break
        case LessThanAssertionKind:
          parts.push(`Is less than ${(step as ILessThanAssertion).value}`)
          break
        case IncludesAssertionKind:
          parts.push(`Includes ${(step as IIncludesAssertion).value}`)
          break
        case NotIncludesAssertionKind:
          parts.push(`Not includes ${(step as INotIncludesAssertion).value}`)
          break
        case OkAssertionKind:
          parts.push(`Is OK`)
          break
        case NotOkAssertionKind:
          parts.push(`Is not OK`)
          break
        default:
      }
    }
    return parts.filter((i) => !!i).join(' > ')
  }

  protected getDataSourceStepLabel(step: IReadDataSourceStep): string {
    if (step.source === FlowSourceEnum.request) {
      if (step.data === FlowRequestDataEnum.url) {
        return 'Request URL'
      }
      if (step.data === FlowRequestDataEnum.headers) {
        return 'Request headers'
      }
      if (step.data === FlowRequestDataEnum.method) {
        return 'HTTP method'
      }
      if (step.data === FlowRequestDataEnum.body) {
        return 'Request body'
      }
    }
    if (step.source === FlowSourceEnum.response) {
      if (step.data === FlowResponseDataEnum.url) {
        return 'Response URL'
      }
      if (step.data === FlowResponseDataEnum.headers) {
        return 'Response headers'
      }
      if (step.data === FlowResponseDataEnum.body) {
        return 'Response body'
      }
      if (step.data === FlowResponseDataEnum.status) {
        return 'Status code'
      }
      if (step.data === FlowResponseDataEnum.statusText) {
        return 'Status message'
      }
    }
    return 'Invalid step'
  }

  protected getValueStepLabel(step: IReadValueStep): string {
    const { path } = step
    return path || ''
  }
}
