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

describe("validation plugin - semantic - 2and3 refs", function() {
  this.timeout(10 * 1000)
  describe("Ref siblings", () => {

    it("should return a warning when another property is a sibling of a $ref in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/CoolPath": {
            get: {
              $ref: "#/components/schemas/abc",
              description: "My very cool get"
            }
          }
        },
        components: {
          schemas: {
            abc: {}
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
        expect(allErrors.length).toEqual(1)
        const firstError = allErrors[0]
        expect(firstError.message).toMatch("Sibling values alongside $refs are ignored.\nTo add properties to a $ref, wrap the $ref into allOf, or move the extra properties into the referenced definition (if applicable).")
        expect(firstError.level).toEqual("warning")
        expect(firstError.path).toEqual(["paths", "/CoolPath", "get", "description"])
      })
    })
    it("should return a warning when another property is a sibling of a $ref in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {
            get: {
              $ref: "#/definitions/abc",
              description: "My very cool get"
            }
          }
        },
        definitions: {
          abc: {}
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
        expect(allErrors.length).toEqual(1)
        const firstError = allErrors[0]
        expect(firstError.message).toMatch("Sibling values alongside $refs are ignored.\nTo add properties to a $ref, wrap the $ref into allOf, or move the extra properties into the referenced definition (if applicable).")
        expect(firstError.level).toEqual("warning")
        expect(firstError.path).toEqual(["paths", "/CoolPath", "get", "description"])
      })
    })

    it("should return no warnings when a $ref has no siblings in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/CoolPath": {
            get: {
              $ref: "#/components/schemas/abc"
            }
          }
        },
        components: {
          schemas: {
            abc: {}
          }
        }
      }

      return expectNoErrorsOrWarnings(spec)
    })
    it("should return no warnings when a $ref has no siblings in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {
            get: {
              $ref: "#/definitions/abc"
            }
          }
        },
        definitions: {
          abc: {}
        }
      }

      return expectNoErrorsOrWarnings(spec)
    })

    it("should return no warnings when a path item $ref has siblings in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          $ref: "#/components/schemas/abc",
          "/CoolPath": {
            get: {
              $ref: "#/components/schemas/abc"
            }
          }
        },
        components: {
          schemas: {
            abc: {}
          }
        }
      }

      return expectNoErrorsOrWarnings(spec)
    })
    it("should return no warnings when a path item $ref has siblings in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          $ref: "#/definitions/abc",
          "/CoolPath": {
            get: {
              $ref: "#/definitions/abc"
            }
          }
        },
        definitions: {
          abc: {}
        }
      }

      return expectNoErrorsOrWarnings(spec)
    })

  })
  describe("Unused definitions", () => {
    it("should return a warning when a definition is declared but not used in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        components: {
          schemas: {
            "x-Foo": {
              type: "object"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
        expect(allErrors.length).toEqual(1)
        const firstError = allErrors[0]
        expect(firstError.message).toMatch("Definition was declared but never used in document")
        expect(firstError.level).toEqual("warning")
        expect(firstError.path).toEqual(["components", "schemas", "x-Foo"])
      })
    })

    it("should not return a warning when a definition with special characters is properly referenced in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/CoolPath": {
            get: {
              responses: {
                200: {
                  content: {
                    "application/json": {
                      schema: {
                        $ref: "#/components/schemas/x~1Foo"
                      }
                    }
                  }
                },
                400: {
                  content: {
                    "application/json": {
                      schema: {
                        $ref: "#/components/schemas/x~0Bar"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        components: {
          schemas: {
            "x/Foo": {
              type: "object"
            },
            "x~Bar": {
              type: "object"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        // We want warnings only, without errors about invalid component names
        const allWarnings = system.errSelectors.allErrors().toJS()
          .filter(err => err.level === "warning")
        expect(allWarnings.length).toEqual(0)
      })
    })

    it("should return a warning when a definition is declared but not used in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {}
        },
        definitions: {
          abc: {
            type: "string"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
        expect(allErrors.length).toEqual(1)
        const firstError = allErrors[0]
        expect(firstError.message).toMatch("Definition was declared but never used in document")
        expect(firstError.level).toEqual("warning")
        expect(firstError.path).toEqual(["definitions", "abc"])
      })
    })

    it("should not return a warning when a definition with special character is declared and used in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {
            get: {
              responses: {
                200: {
                  schema: {
                    $ref: "#/definitions/x~1Foo"
                  }
                },
                400: {
                  schema: {
                    $ref: "#/definitions/x~0Bar"
                  }
                }
              }
            }
          }
        },
        definitions: {
          "x/Foo": {
            type: "object"
          },
          "x~Bar": {
            type: "object"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
        expect(allErrors.length).toEqual(0)
      })
    })

  })
  describe("Malformed $ref values", () => {
    it("should return an error when a JSON pointer lacks a leading `#/` in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {
            $ref: "#myObj/abc"
          }
        },
        myObj: {
          abc: {
            type: "string"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
        expect(allErrors.length).toEqual(1)
        const firstError = allErrors[0]
        expect(firstError.message).toMatch("$ref paths must begin with `#/`")
        expect(firstError.level).toEqual("error")
        expect(firstError.path).toEqual(["paths", "/CoolPath", "$ref"])
      })
    })
    it("should return an error when a JSON pointer lacks a leading `#/` in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/CoolPath": {
            $ref: "#myObj/abc"
          }
        },
        myObj: {
          abc: {
            type: "string"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
        expect(allErrors.length).toEqual(1)
        const firstError = allErrors[0]
        expect(firstError.message).toMatch("$ref paths must begin with `#/`")
        expect(firstError.level).toEqual("error")
        expect(firstError.path).toEqual(["paths", "/CoolPath", "$ref"])
      })
    })
    it("should return no errors when a JSON pointer is a well-formed remote reference in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {
            $ref: "http://google.com#/myObj/abc"
          },
        },
        myObj: {
          abc: {
            type: "string",
            properties: {
              $ref: "http://google.com/MyRegularURLReference"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allSemanticErrors).toEqual([])
      })
    })
    it("should return no errors when a JSON pointer is a well-formed remote reference in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/CoolPath": {
            $ref: "http://google.com#/myObj/abc"
          },
        },
        myObj: {
          abc: {
            type: "string",
            properties: {
              $ref: "http://google.com/MyRegularURLReference"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allSemanticErrors).toEqual([])
      })
    })
    it("should return an error when a JSON pointer uses incorrect percent-encoding in Swagger 2", () => {
      const spec = {
        "swagger": "2.0",
        "paths": {
          "/foo": {
            "get": {
              "responses": {
                "200": {
                  "description": "Success",
                  "schema": {
                    "$ref": "#/definitions/foo bar"
                  }
                }
              }
            }
          }
        },
        "definitions": {
          "foo bar": {
            "type": "string"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
          expect(allSemanticErrors[0]).toInclude({
            level: "warning",
            message: "Definition was declared but never used in document",
            path: ["definitions", "foo bar"]
          })
          expect(allSemanticErrors.length).toEqual(2)
          expect(allSemanticErrors[1]).toInclude({
            level: "error",
            message: "$ref values must be RFC3986-compliant percent-encoded URIs",
            path: ["paths", "/foo", "get", "responses", "200", "schema", "$ref"]
          })
       })
    })
    it("should return an error when a JSON pointer uses incorrect percent-encoding in OpenAPI 3", () => {
      const spec = {
        "openapi": "3.0.0",
        "paths": {
          "/foo": {
            "get": {
              "responses": {
                "200": {
                  "description": "Success",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/foo bar"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        components: {
          schemas: {
            "foo bar": {
              "type": "string"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
          expect(allSemanticErrors.length).toEqual(3)
          expect(allSemanticErrors[0]).toInclude({
            level: "warning",
            message: "Definition was declared but never used in document",
            path: ["components", "schemas", "foo bar"]
          })
          expect(allSemanticErrors[1]).toInclude({
            level: "error",
            message: "$ref values must be RFC3986-compliant percent-encoded URIs",
            path: ["paths", "/foo", "get", "responses", "200", "content", "application/json", "schema", "$ref"]
          }),
          expect(allSemanticErrors[2]).toInclude({
            level: "error",
            message: "Component names can only contain the characters A-Z a-z 0-9 - . _",
            path: ["components", "schemas", "foo bar"]
          })
      })
    })
    it("should return no errors when a JSON pointer uses correct percent-encoding in Swagger 2", () => {
      const spec = {
        "swagger": "2.0",
        "paths": {
          "/foo": {
            "get": {
              "responses": {
                "200": {
                  "description": "Success",
                  "schema": {
                    "$ref": "#/definitions/foo%20bar"
                  }
                }
              }
            }
          }
        },
        "definitions": {
          "foo bar": {
            "type": "string"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allSemanticErrors).toEqual([])
      })
    })
    it("should return no errors when a JSON pointer uses correct percent-encoding in OpenAPI 3", () => {
      const spec = {
        "openapi": "3.0.0",
        "paths": {
          "/foo": {
            "get": {
              "responses": {
                "200": {
                  "description": "Success",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/foo%20bar"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        components: {
          schemas: {
            "foo bar": {
              "type": "string"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allSemanticErrors.length).toEqual(1)
        expect(allSemanticErrors[0]).toInclude({
          level: "error",
          message: "Component names can only contain the characters A-Z a-z 0-9 - . _",
          path: ["components", "schemas", "foo bar"]
        })
      })
    })

  })
  describe("Nonexistent $ref pointers", () => {
    it("should return an error when a local JSON pointer does not exist in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {
            $ref: "#/myObj/DoesNotExist"
          }
        },
        myObj: {
          abc: {
            type: "string",
            properties: {
              $ref: "http://google.com/MyRegularURLReference"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allErrors.length).toEqual(1)
        const firstError = allErrors[0]
        expect(firstError.message).toMatch("$refs must reference a valid location in the document")
        expect(firstError.level).toEqual("error")
        expect(firstError.path).toEqual(["paths", "/CoolPath", "$ref"])
      })
    })
    it("should return an error when a local JSON pointer does not exist in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/CoolPath": {
            $ref: "#/myObj/DoesNotExist"
          }
        },
        myObj: {
          abc: {
            type: "string",
            properties: {
              $ref: "http://google.com/MyRegularURLReference"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
          .filter((el, i, arr) => arr.indexOf(el) === i)
        expect(allErrors.length).toEqual(1)
        const firstError = allErrors[0]
        expect(firstError.message).toMatch("$refs must reference a valid location in the document")
        expect(firstError.level).toEqual("error")
        expect(firstError.path).toEqual(["paths", "/CoolPath", "$ref"])
      })
    })
    it("should return no errors when a JSON pointer exists in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {
            $ref: "#/myObj/abc"
          },
        },
        myObj: {
          abc: {
            type: "string"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allSemanticErrors).toEqual([])
      })
    })
    it("should return no errors when a JSON pointer exists in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/CoolPath": {
            $ref: "#/myObj/abc"
          },
        },
        myObj: {
          abc: {
            type: "string",
            properties: {
              $ref: "http://google.com/MyRegularURLReference"
            }
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allSemanticErrors).toEqual([])
      })
    })
    it("should return no errors when a JSON pointer is a remote reference in Swagger 2", () => {
      const spec = {
        swagger: "2.0",
        paths: {
          "/CoolPath": {
            $ref: "http://google.com#/myObj/abc"
          },
        },
        myObj: {
          abc: {
            type: "string"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allSemanticErrors).toEqual([])
      })
    })
    it("should return no errors when a JSON pointer is a remote reference in OpenAPI 3", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/CoolPath": {
            $ref: "http://google.com#/myObj/abc"
          },
        },
        myObj: {
          abc: {
            type: "string"
          }
        }
      }

      return validateHelper(spec)
      .then(system => {
        const allSemanticErrors = system.errSelectors.allErrors().toJS()
          .filter(err => err.source !== "resolver")
        expect(allSemanticErrors).toEqual([])
      })
    })
  })
})
