class RequestAPITest(TestHandlerBase):
    def setUp(self):
        self.request_mock = Mock()

        self.ts_epoch = 1451606400000
        self.ts_dt = datetime.datetime(2016, 1, 1)
        self.request_dict = {
            "children": [],
            "parent": None,
            "system": "system_name",
            "system_version": "0.0.1",
            "instance_name": "default",
            "command": "say",
            "id": "58542eb571afd47ead90d25f",
            "parameters": {},
            "comment": "bye!",
            "output": "nested output",
            "output_type": "STRING",
            "status": "IN_PROGRESS",
            "command_type": "ACTION",
            "created_at": self.ts_epoch,
            "updated_at": self.ts_epoch,
            "error_class": None,
            "metadata": {},
            "has_parent": True,
            "requester": None,
        }
        self.job_dict = {
            "name": "job_name",
            "trigger_type": "date",
            "trigger": {"run_date": self.ts_epoch, "timezone": "utc"},
            "request_template": {
                "system": "system",
                "system_version": "1.0.0",
                "instance_name": "default",
                "command": "speak",
                "parameters": {"message": "hey!"},
                "comment": "hi!",
                "metadata": {"request": "stuff"},
            },
            "misfire_grace_time": 3,
            "coalesce": True,
            "next_run_time": self.ts_epoch,
            "success_count": 0,
            "error_count": 0,
        }
        db_dict = copy.deepcopy(self.job_dict)
        db_dict["request_template"] = RequestTemplate(**db_dict["request_template"])
        db_dict["trigger"]["run_date"] = self.ts_dt
        db_dict["trigger"] = DateTrigger(**db_dict["trigger"])
        db_dict["next_run_time"] = self.ts_dt
        self.job = Job(**db_dict)

        db_dict = copy.deepcopy(self.request_dict)
        db_dict["created_at"] = self.ts_dt
        db_dict["updated_at"] = self.ts_dt
        self.request = Request(**db_dict)

        super(RequestAPITest, self).setUp()

    def tearDown(self):
        Request.objects.delete()
        Job.objects.delete()

    def test_get(self):
        self.request.save()
        response = self.fetch("/api/v1/requests/" + str(self.request.id))
        self.assertEqual(200, response.code)
        data = json.loads(response.body.decode("utf-8"))
        data.pop("updated_at")
        self.request_dict.pop("updated_at")
        self.assertEqual(self.request_dict, data)

    def test_patch_replace_duplicate(self):
        self.request.status = "SUCCESS"
        self.request.output = "output"
        self.request.save()
        body = json.dumps(
            {
                "operations": [
                    {"operation": "replace", "path": "/output", "value": "output"},
                    {"operation": "replace", "path": "/status", "value": "SUCCESS"},
                ]
            }
        )

        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.assertEqual(200, response.code)

        self.request.reload()
        self.assertEqual("SUCCESS", self.request.status)
        self.assertEqual("output", self.request.output)

    def test_patch_replace_status(self):
        self.request.save()
        body = json.dumps(
            {
                "operations": [
                    {"operation": "replace", "path": "/status", "value": "SUCCESS"}
                ]
            }
        )

        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.assertEqual(200, response.code)
        self.request.reload()
        self.assertEqual("SUCCESS", self.request.status)

    def test_patch_replace_output(self):
        self.request.output = "old_output_but_not_done_with_progress"
        self.request.save()
        body = json.dumps(
            {
                "operations": [
                    {"operation": "replace", "path": "/output", "value": "output"}
                ]
            }
        )

        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.assertEqual(200, response.code)
        self.request.reload()
        self.assertEqual("output", self.request.output)

    def test_patch_replace_error_class(self):
        self.request.error_class = "Klazz1"
        body = json.dumps(
            {
                "operations": [
                    {"operation": "replace", "path": "/error_class", "value": "error"}
                ]
            }
        )
        self.request.save()

        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.request.reload()
        self.assertEqual(200, response.code)
        self.assertEqual("error", self.request.error_class)

    def test_patch_replace_bad_status(self):
        self.request.save()
        body = json.dumps(
            {
                "operations": [
                    {"operation": "replace", "path": "/status", "value": "bad"}
                ]
            }
        )
        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.assertGreaterEqual(response.code, 400)

    def test_patch_update_output_for_complete_request(self):
        self.request.status = "SUCCESS"
        self.request.output = "old_value"
        self.request.save()
        body = json.dumps(
            {
                "operations": [
                    {
                        "operation": "replace",
                        "path": "/output",
                        "value": "shouldnt work",
                    }
                ]
            }
        )
        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.request.reload()
        self.assertGreaterEqual(response.code, 400)
        self.assertEqual(self.request.output, "old_value")

    def test_patch_no_system(self):
        good_id_does_not_exist = "".join("1" for _ in range(24))
        response = self.fetch(
            "/api/v1/requests/" + good_id_does_not_exist,
            method="PATCH",
            body='{"operations": [{"operation": "fake"}]}',
            headers={"content-type": "application/json"},
        )
        self.assertEqual(response.code, 404)

    def test_patch_replace_bad_path(self):
        self.request.save()
        body = json.dumps(
            {"operations": [{"operation": "replace", "path": "/bad", "value": "error"}]}
        )
        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.assertGreaterEqual(response.code, 400)

    def test_patch_bad_operation(self):
        self.request.save()
        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body='{"operations": [{"operation": "fake"}]}',
            headers={"content-type": "application/json"},
        )
        self.assertGreaterEqual(response.code, 400)

    def test_prometheus_endpoint(self):
        handler = self.app.find_handler(request=Mock(path="/api/v1/requests"))
        c = handler.handler_class(
            self.app, Mock(path="/api/v1/requests/111111111111111111111111")
        )
        assert c.prometheus_endpoint == "/api/v1/requests/<ID>"

    def test_update_job_numbers(self):
        self.job.save()
        self.request.metadata["_bg_job_id"] = str(self.job.id)
        self.request.save()
        body = json.dumps(
            {
                "operations": [
                    {"operation": "replace", "path": "/status", "value": "SUCCESS"}
                ]
            }
        )
        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.assertEqual(response.code, 200)
        self.job.reload()
        self.assertEqual(self.job.success_count, 1)
        self.assertEqual(self.job.error_count, 0)

    def test_update_job_numbers_error(self):
        self.job.save()
        self.request.metadata["_bg_job_id"] = str(self.job.id)
        self.request.save()
        body = json.dumps(
            {
                "operations": [
                    {"operation": "replace", "path": "/status", "value": "ERROR"}
                ]
            }
        )
        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.assertEqual(response.code, 200)
        self.job.reload()
        self.assertEqual(self.job.success_count, 0)
        self.assertEqual(self.job.error_count, 1)

    def test_update_job_invalid_id(self):
        self.request.metadata["_bg_job_id"] = "".join(["1" for _ in range(24)])
        self.request.save()
        body = json.dumps(
            {
                "operations": [
                    {"operation": "replace", "path": "/status", "value": "ERROR"}
                ]
            }
        )
        response = self.fetch(
            "/api/v1/requests/" + str(self.request.id),
            method="PATCH",
            body=body,
            headers={"content-type": "application/json"},
        )
        self.assertEqual(response.code, 200)
    def post(self):
        """
        ---
        summary: Create a new Request
        parameters:
          - name: request
            in: body
            description: The Request definition
            schema:
              $ref: '#/definitions/Request'
          - name: blocking
            in: query
            required: false
            description: Flag indicating whether to wait for request completion
            type: boolean
            default: false
          - name: timeout
            in: query
            required: false
            description: Maximum time (seconds) to wait for request completion
            type: integer
            default: None (Wait forever)
        consumes:
          - application/json
          - application/x-www-form-urlencoded
        responses:
          201:
            description: A new Request has been created
            schema:
              $ref: '#/definitions/Request'
            headers:
              Instance-Status:
                type: string
                description: |
                    Current status of the Instance that will process the
                    created Request
          400:
            $ref: '#/definitions/400Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Requests
        """
        self.request.event.name = Events.REQUEST_CREATED.name

        if self.request.mime_type == "application/json":
            request_model = self.parser.parse_request(
                self.request.decoded_body, from_string=True)
        elif self.request.mime_type == "application/x-www-form-urlencoded":
            args = {"parameters": {}}
            for key, value in self.request.body_arguments.items():
                if key.startswith("parameters."):
                    args["parameters"][key.replace("parameters.",
                                                   "")] = value[0].decode(
                                                       self.request.charset)
                else:
                    args[key] = value[0].decode(self.request.charset)
            request_model = Request(**args)
        else:
            raise ModelValidationError(
                "Unsupported or missing content-type header")

        if request_model.parent:
            request_model.parent = Request.objects.get(
                id=str(request_model.parent.id))
            if request_model.parent.status in Request.COMPLETED_STATUSES:
                raise ConflictError("Parent request has already completed")
            request_model.has_parent = True
        else:
            request_model.has_parent = False

        if self.current_user:
            request_model.requester = self.current_user.username

        # Ok, ready to save
        request_model.save()
        request_id = str(request_model.id)

        # Set up the wait event BEFORE yielding the processRequest call
        blocking = self.get_argument("blocking", default="").lower() == "true"
        if blocking:
            brew_view.request_map[request_id] = Event()

        with thrift_context() as client:
            try:
                yield client.processRequest(request_id)
            except bg_utils.bg_thrift.InvalidRequest as ex:
                request_model.delete()
                raise ModelValidationError(ex.message)
            except bg_utils.bg_thrift.PublishException as ex:
                request_model.delete()
                raise RequestPublishException(ex.message)
            except Exception:
                if request_model.id:
                    request_model.delete()
                raise

        # Query for request from body id
        req = Request.objects.get(id=request_id)

        # Now attempt to add the instance status as a header.
        # The Request is already created at this point so it's a best-effort thing
        self.set_header("Instance-Status", "UNKNOWN")

        try:
            # Since request has system info we can query for a system object
            system = System.objects.get(name=req.system,
                                        version=req.system_version)

            # Loop through all instances in the system until we find the instance that
            # matches the request instance
            for instance in system.instances:
                if instance.name == req.instance_name:
                    self.set_header("Instance-Status", instance.status)

        # The Request is already created at this point so adding the Instance status
        # header is a best-effort thing
        except Exception as ex:
            self.logger.exception(
                "Unable to get Instance status for Request %s: %s", request_id,
                ex)

        self.request.event_extras = {"request": req}

        # Metrics
        request_created(request_model)

        if blocking:
            # Publish metrics and event here here so they aren't skewed
            # See https://github.com/beer-garden/beer-garden/issues/190
            self.request.publish_metrics = False
            http_api_latency_total.labels(
                method=self.request.method.upper(),
                route=self.prometheus_endpoint,
                status=self.get_status(),
            ).observe(request_latency(self.request.created_time))

            self.request.publish_event = False
            brew_view.event_publishers.publish_event(
                self.request.event, **self.request.event_extras)

            try:
                timeout = self.get_argument("timeout", default=None)
                delta = timedelta(seconds=int(timeout)) if timeout else None

                event = brew_view.request_map.get(request_id)

                yield event.wait(delta)

                request_model.reload()
            except TimeoutError:
                raise TimeoutExceededError("Timeout exceeded for request %s" %
                                           request_id)
            finally:
                brew_view.request_map.pop(request_id, None)

        self.set_status(201)
        self.write(
            self.parser.serialize_request(request_model, to_string=False))