import expect from "expect"
import validateHelper, {
  expectNoErrors,
  expectNoErrorsOrWarnings
} from "./validate-helper.js"

describe("validation plugin - semantic - schema", function() {
  this.timeout(10 * 1000)

  it("should return an error when a definition's property is readOnly and required by the schema", () => {
    const spec = {
      blah: {
        $ref: "#/definitions/CoolModel"
      },
      definitions: {
        CoolModel: {
          required: ["BadProperty"],
          properties: {
            BadProperty: {
              readOnly: true
            }
          }
        }
      }
    }

    return validateHelper(spec)
        .then( system => {
          const allErrors = system.errSelectors.allErrors().toJS()
          expect(allErrors.length).toEqual(1)
          const firstError = allErrors[0]
          expect(firstError.message).toEqual("Read only properties cannot be marked as required by a schema.")
          expect(firstError.path).toEqual(["definitions", "CoolModel", "required", "0"])
        })

  })

  it("should not return an error when a definition's property is not readOnly and required by the schema", () => {
    const spec = {
      definitions: {
        CoolModel: {
          required: ["BadProperty"],
          properties: {
            BadProperty: {
              type: "string"
            }
          }
        }
      }
    }


    return validateHelper(spec)
        .then( system => {
          const allErrors = system.errSelectors
                .allErrors()
                .filter(a => a.get("level") === "error") // We have an incidental "warning"
                .toJS()
          expect(allErrors).toEqual(0)
        })
  })

  it("should return an error when a response schema's property is readOnly and required by the schema", () => {
    const spec = {
      paths: {
        "/CoolPath": {
          get: {
            responses: {
              200: {
                schema: {
                  required: ["BadProperty"],
                  properties: {
                    BadProperty: {
                      readOnly: true
                    }
                  }
                }
              }
            }
          }
        }
      }
    }

    return validateHelper(spec)
        .then( system => {
          const allErrors = system.errSelectors
                .allErrors()
                .toJS()

          expect(allErrors.length).toEqual(1)
          const firstError = allErrors[0]

          expect(firstError.message).toEqual("Read only properties cannot be marked as required by a schema.")
          expect(firstError.path).toEqual(["paths", "/CoolPath", "get", "responses", "200", "schema", "required", "0"])
        })
  })

  it("should not return an error when a response schema's property is not readOnly and required by the schema", () => {
    const spec = {
      paths: {
        "/CoolPath": {
          get: {
            responses: {
              200: {
                schema: {
                  required: ["BadProperty"],
                  properties: {
                    BadProperty: {
                      type: "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }

    return expectNoErrorsOrWarnings(spec)
  })

  it("should return an error when a parameter schema's property is readOnly and required by the schema", () => {
    const spec = {
      paths: {
        "/CoolPath": {
          get: {
            parameters: [{
              name: "BadParameter",
              in: "body",
              schema: {
                required: ["BadProperty"],
                properties: {
                  BadProperty: {
                    readOnly: true
                  }
                }
              }
            }]
          }
        }
      }
    }

    return validateHelper(spec)
        .then( system => {
          const allErrors = system.errSelectors
                .allErrors()
                .toJS()

          expect(allErrors.length).toEqual(1)
          const firstError = allErrors[0]
          expect(firstError.message).toEqual("Read only properties cannot be marked as required by a schema.")
          expect(firstError.path).toEqual(["paths", "/CoolPath", "get", "parameters", "0", "schema", "required", "0"])
        })
  })

  it("should not return an error when a parameter schema's property is not readOnly and required by the schema", () => {
    const spec = {
      paths: {
        "/CoolPath": {
          get: {
            parameters: [{
              name: "BadParameter",
              in: "body",
              schema: {
                required: ["BadProperty"],
                properties: {
                  BadProperty: {
                    type: "string"
                  }
                }
              }
            }]
          }
        }
      }
    }

    return expectNoErrorsOrWarnings(spec)
  })

  describe("Type key", () => {
    it("should return an error when \"type\" is an array", () => {
      const spec = {
        paths: {
          "/CoolPath": {
            get: {
              responses: {
                "200": {
                  schema: {
                    type: ["number", "string"]
                  }
                }
              }
            }
          }
        }
      }

      return validateHelper(spec)
          .then( system => {
            const allErrors = system.errSelectors
                  .allErrors()
                  .toJS()

            expect(allErrors.length).toEqual(1)
            const firstError = allErrors[0]
            expect(firstError.message).toEqual(`Schema "type" key must be a string`)
            expect(firstError.path).toEqual(["paths", "/CoolPath", "get", "responses", "200", "schema", "type"])
          })
    })
    it("should not return an error when \"type\" is a property name", () => {
      const spec = {
        "definitions": {
          "ApiResponse": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer",
                "format": "int32"
              },
              "type": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      }

      return validateHelper(spec)
        .then(system => {
          let allErrors = system.errSelectors.allErrors().toJS()
          allErrors = allErrors.filter(a => a.level != "warning") // ignore warnings
          expect(allErrors.length).toEqual(0)
        })
    })
    it("should not return an error when \"type\" is a property name inside additionalProperties", () => {
      const spec = {
        "definitions": {
          "ApiResponse": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "integer",
                  "format": "int32"
                },
                "type": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      }

      return validateHelper(spec)
        .then(system => {
          let allErrors = system.errSelectors.allErrors().toJS()
          allErrors = allErrors.filter(a => a.level != "warning") // ignore warnings
          expect(allErrors.length).toEqual(0)
        })
    })
    it("should not return an error when \"type\" is a model name", () => {
      const spec = {
        "definitions": {
          "type": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer",
                "format": "int32"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      }

      return expectNoErrors(spec)
    })
  })

  describe("Minimums and maximums", () => {
    it("should return an error when minimum is more than maximum", () => {
      const spec = {
        definitions: {
          MyNumber: {
            minimum: 5,
            maximum: 2
          }
        }
      }

      return validateHelper(spec)
        .then(system => {
          let allErrors = system.errSelectors.allErrors().toJS()
          allErrors = allErrors.filter( a => a.level == "error") // Ignore warnings
          expect(allErrors.length).toEqual(1)
          const firstError = allErrors[0]
          expect(firstError.message).toMatch(/.*minimum.*lower.*maximum.*/)
          expect(firstError.path).toEqual(["definitions", "MyNumber", "minimum"])
        })
    })
    it("should not return an error when minimum is less than maximum", () => {
      const spec = {
        definitions: {
          MyNumber: {
            minimum: 1,
            maximum: 2
          }
        }
      }
      return expectNoErrors(spec)
    })
    it("should return an error when minProperties is more than maxProperties", () => {
      const spec = {
        definitions: {
          MyNumber: {
            minProperties: 5,
            maxProperties: 2
          }
        }
      }

      return validateHelper(spec)
        .then(system => {
          let allErrors = system.errSelectors.allErrors().toJS()
          allErrors = allErrors.filter(a => a.level === "error") // ignore warnings
          expect(allErrors.length).toEqual(1)
          const firstError = allErrors[0]
          expect(firstError.message).toMatch(/.*minProperties.*lower.*maxProperties.*/)
          expect(firstError.path).toEqual(["definitions", "MyNumber", "minProperties"])
        })
    })
    it("should not return an error when minProperties is less than maxProperties", () => {
      const spec = {
        definitions: {
          MyNumber: {
            minProperties: "1",
            maxProperties: "2"
          }
        }
      }

      return expectNoErrors(spec)
    })
    it("should return an error when minLength is more than maxLength", () => {
      const spec = {
        definitions: {
          MyNumber: {
            minLength: 5,
            maxLength: 2
          }
        }
      }

      return validateHelper(spec)
        .then(system => {
          let allErrors = system.errSelectors.allErrors().toJS()
          allErrors = allErrors.filter(a => a.level === "error") // ignore warnings
          expect(allErrors.length).toEqual(1)
          const firstError = allErrors[0]
          expect(firstError.path).toEqual(["definitions", "MyNumber", "minLength"])
          expect(firstError.message).toMatch(/.*minLength.*lower.*maxLength.*/)
        })
    })
    it("should not return an error when minLength is less than maxLength", () => {
      const spec = {
        definitions: {
          MyNumber: {
            minLength: "1",
            maxLength: "2"
          }
        }
      }

      return expectNoErrors(spec)
    })
  })

  describe(`"type: array" requires "items"`, () => {
    describe("header objects", function() {
      // It takes a while to start up swagger-ui, for some reason

      it("should return an error when an array header object omits an `items` property", () => {

        // Given
        const spec = {
          "swagger": "2.0",
          "info": {
            "version": "1.0.0",
            "title": "Swagger Petstore"
          },
          "paths": {
            "/pets": {
              "get": {
                "description": "Returns all pets from the system that the user has access to",
                "responses": {
                  "200": {
                    "description": "pet response",
                    "headers": {
                      "X-MyHeader": {
                        "type": "array"
                      }
                    }
                  },
                  "default": {
                    "description": "unexpected error"
                  }
                }
              }
            }
          }
        }

        // When
        return validateHelper(spec)
          .then(system => {

            // Then
            expect(system.errSelectors.allErrors().count()).toEqual(1)
            const firstError = system.errSelectors.allErrors().first().toJS()
            expect(firstError.message).toMatch(/.*type.*array.*require.*items/)
            expect(firstError.path).toEqual(["paths", "/pets", "get", "responses", "200", "headers", "X-MyHeader"])
          })

      })

      it("should not return an error when an array header object has an `items` property", () => {
        const spec = {
          "swagger": "2.0",
          "info": {
            "version": "1.0.0",
            "title": "Swagger Petstore"
          },
          "paths": {
            "/pets": {
              "get": {
                "description": "Returns all pets from the system that the user has access to",
                "responses": {
                  "200": {
                    "description": "pet response",
                    "headers": {
                      "X-MyHeader": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "default": {
                    "description": "unexpected error"
                  }
                }
              }
            }
          }
        }

        return expectNoErrorsOrWarnings(spec)
      })
    })
    describe("definitions", function() {
      // It takes a while to start up swagger-ui, for some reason

      it("should return an error when an array definition omits an `items` property", () => {

        // Given
        const spec = {
          paths: {
            $ref: "#/definitions/asdf"
          },
          "definitions": {
            "asdf": {
              type: "array"
            }
          }
        }

        // When
        return validateHelper(spec)
          .then(system => {

            // Then
            expect(system.errSelectors.allErrors().count()).toEqual(1)
            const firstError = system.errSelectors.allErrors().first().toJS()
            expect(firstError.message).toMatch(/.*type.*array.*require.*items/)
            expect(firstError.path).toEqual(["definitions", "asdf"])
          })

      })

      it("should not return an error when an array definition has an `items` property", () => {
        const spec = {
          "swagger": "2.0",
          "info": {
            "version": "1.0.0",
            "title": "Swagger Petstore"
          },
          "paths": {
            "/pets": {
              "get": {
                "description": "Returns all pets from the system that the user has access to",
                "responses": {
                  "200": {
                    "description": "pet response",
                    "headers": {
                      "X-MyHeader": {
                        $ref: "#/definitions/array"
                      }
                    }
                  },
                  "default": {
                    "description": "unexpected error"
                  }
                }
              }
            }
          },
          "definitions": {
            "array": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }

        return expectNoErrorsOrWarnings(spec)
      })
    })
    describe("'pattern' Z anchors", function() {
      it("should return an error when a schema has a Z anchor in its pattern", () => {

        // Given
        const spec = {
          paths: {
            $ref: "#/definitions/asdf"
          },
          "definitions": {
            "asdf": {
              type: "string",
              pattern: "^[-a-zA-Z0-9_]+\\Z"
            }
          }
        }

        // When
        return validateHelper(spec)
          .then(system => {

            // Then
            expect(system.errSelectors.allErrors().count()).toEqual(1)
            const firstError = system.errSelectors.allErrors().first().toJS()
            expect(firstError.message).toEqual(`"\\Z" anchors are not allowed in regular expression patterns`)
            expect(firstError.path).toEqual(["definitions", "asdf", "pattern"])
          })

      })
      it("should return an error when a subschema has a Z anchor in its pattern", () => {

        // Given
        const spec = {
          paths: {
            "/": {
              get: {
                responses: {
                  "200": {
                    schema: {
                      type: "object",
                      properties: {
                        slug: {
                          type: "string",
                          pattern: "^[-a-zA-Z0-9_]+\\Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
        }

        // When
        return validateHelper(spec)
          .then(system => {
            // Then
            expect(system.errSelectors.allErrors().count()).toEqual(1)
            const firstError = system.errSelectors.allErrors().first().toJS()
            expect(firstError.message).toEqual(`"\\Z" anchors are not allowed in regular expression patterns`)
            expect(firstError.path).toEqual(["paths", "/", "get", "responses", "200", "schema", "properties", "slug", "pattern"])
          })

      })

      it("should not return an error when a regex pattern doesn't use a Z anchor", () => {
        const spec = {
          paths: {
            $ref: "#/definitions/asdf"
          },
          "definitions": {
            "asdf": {
              type: "string",
              pattern: "^[-a-zA-Z0-9_]+"
            }
          }
        }

        return expectNoErrorsOrWarnings(spec)
      })
    })
  })
})
