import expect from "expect"
import SwaggerUi from "swagger-ui"
import ValidateBasePlugin from "plugins/validate-base"
import ValidateSemanticPlugin from "plugins/validate-semantic"
import ASTPlugin from "plugins/ast"

function getSystem(spec) {
  return new Promise((resolve) => {
    const system = SwaggerUi({
      spec,
      domNode: null,
      presets: [
        SwaggerUi.plugins.SpecIndex,
        SwaggerUi.plugins.ErrIndex,
        SwaggerUi.plugins.DownloadUrl,
        SwaggerUi.plugins.SwaggerJsIndex,
      ],
      initialState: {
        layout: undefined
      },
      plugins: [
        ASTPlugin,
        ValidateBasePlugin,
        ValidateSemanticPlugin,
        () => ({
          statePlugins: {
            configs: {
              actions: {
                loaded: () => {
                  return {
                    type: "noop"
                  }
                }
              }
            }
          }
        })
      ]
    })
    resolve(system)
  })
}

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

  describe("allSchemas", function() {
    describe("OpenAPI 2.0", function() {
      it("should pick up parameter schemas", () => {
        const spec = {
          paths: {
            test: {
              parameters: [{
                name: "common"
              }],
              get: {
                parameters: [{
                  name: "tags"
                }]
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(2)
            expect(nodes[0].path).toEqual(["paths","test","parameters","0"])
            expect(nodes[1].path).toEqual(["paths","test","get","parameters","0"])
          })
      })

      it("should pick up response schemas", () => {
        const spec = {
          paths: {
            test: {
              get: {
                responses: {
                  "200": {
                    schema: {
                      type: "string"
                    }
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(1)
            expect(nodes[0].path.join(".")).toEqual("paths.test.get.responses.200.schema")
          })
      })

      it("should pick up global definitions", () => {
        const spec = {
          definitions: {
            fooModel: {
              type: "object",
              properties: {
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(1)
            expect(nodes[0].key).toEqual("fooModel")
          })
      })

      it("should pick up global definitions named 'x-' (i.e. not consider them extensions)", () => {
        const spec = {
          definitions: {
            "x-fooModel": {
              type: "string"
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(1)
            expect(nodes[0].key).toEqual("x-fooModel")
          })
      })

      it("should pick up response headers", () => {
        const spec = {
          paths: {
            test: {
              get: {
                responses: {
                  "200": {
                    headers: {
                      foo: {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(1)
            expect(nodes[0].path.join(".")).toEqual("paths.test.get.responses.200.headers.foo")
          })
      })

      it("should pick up subschemas in properties", () => {
        const spec = {
          definitions: {
            fooModel: {
              type: "object",
              properties: {
                foo: {
                  type: "string"
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(2)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("foo")
          })
      })

      it("should pick up subschemas in additionalProperties - simple", () => {
        const spec = {
          definitions: {
            fooModel: {
              type: "object",
              additionalProperties: {
                type: "string"
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(2)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("additionalProperties")
          })
      })

      it("should pick up subschemas in additionalProperties - complex", () => {
        const spec = {
          definitions: {
            fooModel: {
              type: "object",
              additionalProperties: {
                type: "object",
                properties: {
                  foo: {
                    type: "string"
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(3)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("additionalProperties")
            expect(nodes[2].key).toEqual("foo")
          })
      })

      it("should pick up subschemas in array", () => {
        const spec = {
          definitions: {
            fooModel: {
              type: "array",
              items: {
                type: "string"
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(2)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("items")
          })
      })

      it("should pick up subschemas in array of objects", () => {
        const spec = {
          definitions: {
            fooModel: {
              type: "array",
              items: {
                type: "object",
                properties: {
                  foo: {
                    type: "string"
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(3)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("items")
            expect(nodes[2].key).toEqual("foo")
          })
      })
    })

    describe("OpenAPI 3.0", function() {
      it("should pick up schemas in 'components'", () => {
        const spec = {
          openapi: "3.0.0",
          components: {
            schemas: {
              fooModel: {
                type: "string"
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(1)
            expect(nodes[0].key).toEqual("fooModel")
          })
      })

      it("should pick up schemas named 'x-' (i.e. not consider them extensions)", () => {
        const spec = {
          openapi: "3.0.0",
          components: {
            schemas: {
              "x-fooModel": {
                type: "string"
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(1)
            expect(nodes[0].key).toEqual("x-fooModel")
          })
      })

      it("should pick up request body schemas and response schemas", () => {
        const spec = {
          "openapi": "3.0.0",
          "paths": {
            "/ping": {
              "post": {
                "requestBody": {
                  "content": {
                    "application/myRequestMediaType": {
                      "schema": {
                        "type": "array"
                      }
                    }
                  }
                },
                "responses": {
                  "200": {
                    "description": "OK",
                    "content": {
                      "application/myResponseMediaType": {
                        "schema": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(2)
            expect(nodes[0].node).toNotBe(nodes[1].node)
            expect(nodes[0].key).toEqual("schema")
            expect(nodes[0].parent.key).toEqual("application/myRequestMediaType")
            expect(nodes[1].key).toEqual("schema")
            expect(nodes[1].parent.key).toEqual("application/myResponseMediaType")
          })
      })

      it("should pick up schemas in response components", () => {
        const spec = {
          openapi: "3.0.0",
          components: {
            responses: {
              MyResponse: {
                content: {
                  "application/json": {
                    schema: {
                      type: "string"
                    }
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(1)
            expect(nodes[0].path).toEqual(["components","responses","MyResponse","content","application/json","schema"])
          })
      })

      it("should pick up subschemas in properties", () => {
        const spec = {
          openapi: "3.0.0",
          components: {
            schemas: {
              fooModel: {
                type: "object",
                properties: {
                  foo: {
                    type: "string"
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(2)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("foo")
          })
      })

      it("should pick up subschemas in additionalProperties - simple", () => {
        const spec = {
          openapi: "3.0.0",
          components: {
            schemas: {
              fooModel: {
                type: "object",
                additionalProperties: {
                  type: "string"
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(2)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("additionalProperties")
          })
      })

      it("should pick up subschemas in additionalProperties - complex", () => {
        const spec = {
          openapi: "3.0.0",
          components: {
            schemas: {
              fooModel: {
                type: "object",
                additionalProperties: {
                  type: "object",
                  properties: {
                    foo: {
                      type: "string"
                    }
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(3)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("additionalProperties")
            expect(nodes[2].key).toEqual("foo")
          })
      })

      it("should pick up subschemas in array", () => {
        const spec = {
          openapi: "3.0.0",
          components: {
            schemas: {
              fooModel: {
                type: "array",
                items: {
                  type: "string"
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(2)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("items")
          })
      })

      it("should pick up subschemas in array of objects", () => {
        const spec = {
          openapi: "3.0.0",
          components: {
            schemas: {
              fooModel: {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    foo: {
                      type: "string"
                    }
                  }
                }
              }
            }
          }
        }

        return getSystem(spec)
          .then(system => system.validateSelectors.allSchemas())
          .then(nodes => {
            expect(nodes.length).toEqual(3)
            expect(nodes[0].key).toEqual("fooModel")
            expect(nodes[1].key).toEqual("items")
            expect(nodes[2].key).toEqual("foo")
          })
      })
    })
  })

  describe("allResponses", function() {
    it("should pick up operation responses with specific codes like 200", () => {
      const spec = {
        paths: {
          "/foo": {
            get: {
              responses: {
                "200": {
                  description: "ok"
                }
              }
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["paths","/foo","get","responses","200"])
        })
    })
    it("should pick up the 'default' response", () => {
      const spec = {
        paths: {
          "/foo": {
            get: {
              responses: {
                default: {
                  description: "ok"
                }
              }
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["paths","/foo","get","responses","default"])
        })
    })
    it("should pick up OAS3 response code ranges like 2XX", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/foo": {
            get: {
              responses: {
                "2XX": {
                  description: "ok"
                }
              }
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["paths","/foo","get","responses","2XX"])
        })
    })
    it("should ignore x- extension keys in an operation's `responses` section", () => {
      const spec = {
        paths: {
          "/foo": {
            get: {
              responses: {
                "x-ext": {
                  foo: "bar"
                }
              }
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(0)
        })
    })
    it("should pick up OpenAPI 2 global response definitions", () => {
      const spec = {
        swagger: "2.0",
        responses: {
          OkResponse: {
            description: "ok"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["responses","OkResponse"])
        })
    })
    it("should pick up OpenAPI 2 response definitions named 'x-'", () => {
      const spec = {
        swagger: "2.0",
        responses: {
          "x-MyResponse": {
            description: "ok"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["responses","x-MyResponse"])
        })
    })
    it("should pick up OpenAPI 3 response components", () => {
      const spec = {
        openapi: "3.0.0",
        components : {
          responses: {
            OkResponse: {
              description: "ok"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["components","responses","OkResponse"])
        })
    })
    it("should pick up OpenAPI 3 response components named 'x-'", () => {
      const spec = {
        openapi: "3.0.0",
        components : {
          responses: {
            "x-MyResponse": {
              description: "ok"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["components","responses","x-MyResponse"])
        })
    })
  })

  describe("allSecurityDefinitions", () => {
    it("should pick up OAS2 security schemes", () => {
      const spec = {
        swagger: "2.0",
        securityDefinitions: {
          basicAuth: {
            type: "basic"
          },
          apiKeyAuth: {
            type: "apiKey"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].key).toEqual("basicAuth")
          expect(nodes[1].key).toEqual("apiKeyAuth")
        })
    })
    it("should pick up OAS3 security schemes", () => {
      const spec = {
        openapi: "3.0.0",
        components: {
          securitySchemes: {
            basicAuth: {
              type: "http"
            },
            apiKeyAuth: {
              type: "apiKey"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].key).toEqual("basicAuth")
          expect(nodes[1].key).toEqual("apiKeyAuth")
        })
    })
    it("should pick up OAS2 security schemes named x- (i.e. not consider them extensions)", () => {
      const spec = {
        swagger: "2.0",
        securityDefinitions: {
          "x-auth": {
            type: "basic"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].key).toEqual("x-auth")
        })
    })
    it("should pick up OAS3 security schemes named x- (i.e. not consider them extensions)", () => {
      const spec = {
        openapi: "3.0.0",
        components: {
          securitySchemes: {
            "x-auth": {
              type: "basic"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].key).toEqual("x-auth")
        })
    })
    it("should not pick up arbitrary OAS2 nodes named `securityDefinitions`", () => {
      const spec = {
        swagger: "2.0",
        definitions: {
          securityDefinitions: {
            type: "object"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(0)
        })
    })
    it("should not pick up arbitrary OAS3 nodes named `securitySchemes`", () => {
      const spec = {
        openapi: "3.0.0",
        components: {
          schemas: {
            securitySchemes: {
              type: "object"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(0)
        })
    })
  })

  describe("allSecurityRequirements", () => {
    it("should pick up global security requirements", () => {
      const spec = {
        security: [
          {
            auth1: []
          },
          {
            auth2: [],
            auth3: []
          }
        ]
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityRequirements())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].node).toEqual(
            { "auth1": [] }
          )
          expect(nodes[1].node).toEqual(
            { "auth2": [], "auth3": [] }
          )
        })
    })
    it("should pick up operation-level security requirements", () => {
      const spec = {
        paths: {
          "/": {
            get: {
              security: [
                {
                  auth1: []
                },
                {
                  auth2: [],
                  auth3: []
                }
              ]
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityRequirements())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].node).toEqual(
            { "auth1": [] }
          )
          expect(nodes[1].node).toEqual(
            { "auth2": [], "auth3": [] }
          )
        })
    })
    it("should pick up empty security requirements", () => {
      const spec = {
        security: [
          {}
        ],
        paths: {
          "/": {
            get: {
              security: [
                {}
              ]
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityRequirements())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].node).toEqual({})
          expect(nodes[1].node).toEqual({})
        })
    })
    it("should not pick up the `security` key inside extensions", () => {
      const spec = {
        paths: {
          "/": {
            "x-ext1": {
              security: [
                { foo: [] }
              ]
            }
          },
          "x-ext2": {
            get: {
              security: [
                { bar: [] }
              ]
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityRequirements())
        .then(nodes => {
          expect(nodes.length).toEqual(0)
        })
    })
  })
})