/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import axios from "axios";
import YAML from "yaml";

const ENDPOINT = "/apisix/admin/configs";
const clientConfig = {
  baseURL: "http://localhost:1984",
  headers: {
    "X-API-KEY": "edd1c9f034335f136f87ad84b625c8f1",
  },
};
const config1 = {
  routes: [
    {
      id: "r1",
      uri: "/r1",
      upstream: {
        nodes: { "127.0.0.1:1980": 1 },
        type: "roundrobin",
      },
      plugins: { "proxy-rewrite": { uri: "/hello" } },
    },
  ],
};
const config2 = {
  routes: [
    {
      id: "r2",
      uri: "/r2",
      upstream: {
        nodes: { "127.0.0.1:1980": 1 },
        type: "roundrobin",
      },
      plugins: { "proxy-rewrite": { uri: "/hello" } },
    },
  ],
};
const invalidConfVersionConfig1 = {
  routes_conf_version: -1,
};
const invalidConfVersionConfig2 = {
  routes_conf_version: "adc",
};
const routeWithModifiedIndex = {
  routes: [
    {
      id: "r1",
      uri: "/r1",
      modifiedIndex: 1,
      upstream: {
        nodes: { "127.0.0.1:1980": 1 },
        type: "roundrobin",
      },
      plugins: { "proxy-rewrite": { uri: "/hello" } },
    },
  ],
};
const routeWithKeyAuth = {
  routes: [
    {
      id: "r1",
      uri: "/r1",
      upstream: {
        nodes: { "127.0.0.1:1980": 1 },
        type: "roundrobin",
      },
      plugins: {
        "proxy-rewrite": { uri: "/hello" },
        "key-auth": {},
      },
    },
  ]
}
const consumerWithModifiedIndex = {
  routes: routeWithKeyAuth.routes,
  consumers: [
    {
      modifiedIndex: 10,
      username: "jack",
      plugins: {
        "key-auth": {
          key: "jack-key",
        }
      },
    },
  ],
}
const credential1 = {
  routes: routeWithKeyAuth.routes,
  consumers: [
    {
      "username": "john_1"
    },
    {
      "id": "john_1/credentials/john-a",
      "plugins": {
        "key-auth": {
          "key": "auth-a"
        }
      }
    },
    {
      "id": "john_1/credentials/john-b",
      "plugins": {
        "key-auth": {
          "key": "auth-b"
        }
      }
    }
  ]
}

describe("Admin - Standalone", () => {
  const client = axios.create(clientConfig);
  client.interceptors.response.use((response) => {
    const contentType = response.headers["content-type"] || "";
    if (
      contentType.includes("application/yaml") &&
      typeof response.data === "string" &&
      response.config.responseType !== "text"
    )
      response.data = YAML.parse(response.data);
    return response;
  });

  describe("Normal", () => {
    it("dump empty config (default json format)", async () => {
      const resp = await client.get(ENDPOINT);
      expect(resp.status).toEqual(200);
      expect(resp.data.routes_conf_version).toEqual(0);
      expect(resp.data.ssls_conf_version).toEqual(0);
      expect(resp.data.services_conf_version).toEqual(0);
      expect(resp.data.upstreams_conf_version).toEqual(0);
      expect(resp.data.consumers_conf_version).toEqual(0);
    });

    it("dump empty config (yaml format)", async () => {
      const resp = await client.get(ENDPOINT, {
        headers: { Accept: "application/yaml" },
      });
      expect(resp.status).toEqual(200);
      expect(resp.headers["content-type"]).toEqual("application/yaml");
      expect(resp.data.routes_conf_version).toEqual(0);
      expect(resp.data.ssls_conf_version).toEqual(0);
      expect(resp.data.services_conf_version).toEqual(0);
      expect(resp.data.upstreams_conf_version).toEqual(0);
      expect(resp.data.consumers_conf_version).toEqual(0);
    });

    it("update config (add routes, by json)", async () => {
      const resp = await client.put(ENDPOINT, config1);
      expect(resp.status).toEqual(202);
    });

    it("dump config (json format)", async () => {
      const resp = await client.get(ENDPOINT);
      expect(resp.status).toEqual(200);
      expect(resp.data.routes_conf_version).toEqual(1);
      expect(resp.data.ssls_conf_version).toEqual(1);
      expect(resp.data.services_conf_version).toEqual(1);
      expect(resp.data.upstreams_conf_version).toEqual(1);
      expect(resp.data.consumers_conf_version).toEqual(1);
    });

    it("check default value", async () => {
      const resp = await client.get(ENDPOINT);
      expect(resp.status).toEqual(200);
      expect(resp.data.routes).toEqual(config1.routes);
    });

    it("dump config (yaml format)", async () => {
      const resp = await client.get(ENDPOINT, {
        headers: { Accept: "application/yaml" },
        responseType: 'text',
      });
      expect(resp.status).toEqual(200);
      expect(resp.data).toContain("routes:")
      expect(resp.data).toContain("id: r1")
      expect(resp.data.startsWith('---')).toBe(false);
      expect(resp.data.endsWith('...')).toBe(false);
    });

    it('check route "r1"', async () => {
      const resp = await client.get("/r1");
      expect(resp.status).toEqual(200);
      expect(resp.data).toEqual("hello world\n");
    });

    it("update config (add routes, by yaml)", async () => {
      const resp = await client.put(
        ENDPOINT,
        YAML.stringify(config2),
        {
          headers: { "Content-Type": "application/yaml" },
        }
      );
      expect(resp.status).toEqual(202);
    });

    it("dump config (json format)", async () => {
      const resp = await client.get(ENDPOINT);
      expect(resp.status).toEqual(200);
      expect(resp.data.routes_conf_version).toEqual(2);
      expect(resp.data.ssls_conf_version).toEqual(2);
      expect(resp.data.services_conf_version).toEqual(2);
      expect(resp.data.upstreams_conf_version).toEqual(2);
      expect(resp.data.consumers_conf_version).toEqual(2);
    });

    it('check route "r1"', () =>
      expect(client.get("/r1")).rejects.toThrow(
        "Request failed with status code 404"
      ));

    it('check route "r2"', async () => {
      const resp = await client.get("/r2");
      expect(resp.status).toEqual(200);
      expect(resp.data).toEqual("hello world\n");
    });

    it("update config (delete routes)", async () => {
      const resp = await client.put(
        ENDPOINT,
        {},
        { params: { conf_version: 3 } }
      );
      expect(resp.status).toEqual(202);
    });

    it('check route "r2"', () =>
      expect(client.get("/r2")).rejects.toThrow(
        "Request failed with status code 404"
      ));

    it("only set routes_conf_version", async () => {
      const resp = await client.put(
        ENDPOINT,
        YAML.stringify({ routes_conf_version: 15 }),
        {
          headers: { "Content-Type": "application/yaml" },
        });
      expect(resp.status).toEqual(202);

      const resp_1 = await client.get(ENDPOINT);
      expect(resp_1.status).toEqual(200);
      expect(resp_1.data.routes_conf_version).toEqual(15);
      expect(resp_1.data.ssls_conf_version).toEqual(4);
      expect(resp_1.data.services_conf_version).toEqual(4);
      expect(resp_1.data.upstreams_conf_version).toEqual(4);
      expect(resp_1.data.consumers_conf_version).toEqual(4);

      const resp2 = await client.put(
        ENDPOINT,
        YAML.stringify({ routes_conf_version: 17 }),
        {
          headers: { "Content-Type": "application/yaml" },
        });
      expect(resp2.status).toEqual(202);

      const resp2_1 = await client.get(ENDPOINT);
      expect(resp2_1.status).toEqual(200);
      expect(resp2_1.data.routes_conf_version).toEqual(17);
      expect(resp2_1.data.ssls_conf_version).toEqual(5);
      expect(resp2_1.data.services_conf_version).toEqual(5);
      expect(resp2_1.data.upstreams_conf_version).toEqual(5);
      expect(resp2_1.data.consumers_conf_version).toEqual(5);
    });

    it("control resource changes using modifiedIndex", async () => {
      const c1 = structuredClone(routeWithModifiedIndex);
      c1.routes[0].modifiedIndex = 1;

      const c2 = structuredClone(c1);
      c2.routes[0].uri = "/r2";

      const c3 = structuredClone(c2);
      c3.routes[0].modifiedIndex = 2;

      // Update with c1
      const resp = await client.put(ENDPOINT, c1);
      expect(resp.status).toEqual(202);

      // Check route /r1 exists
      const resp_1 = await client.get("/r1");
      expect(resp_1.status).toEqual(200);

      // Update with c2
      const resp2 = await client.put(ENDPOINT, c2);
      expect(resp2.status).toEqual(202);

      // Check route /r1 exists
      // But it is not applied because the modifiedIndex is the same as the old value
      const resp2_2 = await client.get("/r1");
      expect(resp2_2.status).toEqual(200);

      // Check route /r2 not exists
      const resp2_1 = await client.get("/r2").catch((err) => err.response);
      expect(resp2_1.status).toEqual(404);

      // Update with c3
      const resp3 = await client.put(ENDPOINT, c3);
      expect(resp3.status).toEqual(202);

      // Check route /r1 not exists
      const resp3_1 = await client.get("/r1").catch((err) => err.response);
      expect(resp3_1.status).toEqual(404);

      // Check route /r2 exists
      const resp3_2 = await client.get("/r2");
      expect(resp3_2.status).toEqual(200);
    });

    it("apply consumer with modifiedIndex", async () => {
      const resp = await client.put(ENDPOINT, consumerWithModifiedIndex);
      expect(resp.status).toEqual(202);

      const resp_1 = await client.get("/r1", { headers: { "apikey": "invalid-key" } }).catch((err) => err.response);
      expect(resp_1.status).toEqual(401);
      const resp_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } });
      expect(resp_2.status).toEqual(200);

      const updatedConsumer = structuredClone(consumerWithModifiedIndex);

      // update key of key-auth plugin, but modifiedIndex is not changed
      updatedConsumer.consumers[0].plugins["key-auth"] = { "key": "jack-key-updated" };
      const resp2 = await client.put(ENDPOINT, updatedConsumer);
      expect(resp2.status).toEqual(202);

      const resp2_1 = await client.get("/r1", { headers: { "apikey": "jack-key-updated" } }).catch((err) => err.response);
      expect(resp2_1.status).toEqual(401);
      const resp2_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } });
      expect(resp2_2.status).toEqual(200);

      // update key of key-auth plugin, and modifiedIndex is changed
      updatedConsumer.consumers[0].modifiedIndex++;
      const resp3 = await client.put(ENDPOINT, updatedConsumer);
      const resp3_1 = await client.get("/r1", { headers: { "apikey": "jack-key-updated" } });
      expect(resp3_1.status).toEqual(200);
      const resp3_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } }).catch((err) => err.response);
      expect(resp3_2.status).toEqual(401);
    });

    it("apply consumer with credentials", async () => {
      const resp = await client.put(ENDPOINT, credential1);
      expect(resp.status).toEqual(202);

      const resp_1 = await client.get("/r1", { headers: { "apikey": "auth-a" } });
      expect(resp_1.status).toEqual(200);
      const resp_2 = await client.get("/r1", { headers: { "apikey": "auth-b" } });
      expect(resp_2.status).toEqual(200);
      const resp_3 = await client.get("/r1", { headers: { "apikey": "invalid-key" } }).catch((err) => err.response);
      expect(resp_3.status).toEqual(401);
    });
  });

  describe("Exceptions", () => {
    const clientException = axios.create({
      ...clientConfig,
      validateStatus: () => true,
    });

    it("update config (lower conf_version)", async () => {
      const resp = await clientException.put(
        ENDPOINT,
        { routes_conf_version: 100 },
        { headers: { "Content-Type": "application/yaml" } }
      );
      const resp2 = await clientException.put(
        ENDPOINT,
        YAML.stringify(invalidConfVersionConfig1),
        { headers: { "Content-Type": "application/yaml" } }
      );
      expect(resp2.status).toEqual(400);
      expect(resp2.data).toEqual({
        error_msg:
          "routes_conf_version must be greater than or equal to (100)",
      });
    });

    it("update config (invalid conf_version)", async () => {
      const resp = await clientException.put(
        ENDPOINT,
        YAML.stringify(invalidConfVersionConfig2),
        {
          headers: {
            "Content-Type": "application/yaml",
          },
        }
      );
      expect(resp.status).toEqual(400);
      expect(resp.data).toEqual({
        error_msg: "routes_conf_version must be a number",
      });
    });

    it("update config (invalid json format)", async () => {
      const resp = await clientException.put(ENDPOINT, "{abcd", {
        params: { conf_version: 4 },
      });
      expect(resp.status).toEqual(400);
      expect(resp.data).toEqual({
        error_msg:
          "invalid request body: Expected object key string but found invalid token at character 2",
      });
    });

    it("update config (not compliant with jsonschema)", async () => {
      const data = structuredClone(config1);
      (data.routes[0].uri as unknown) = 123;
      const resp = await clientException.put(ENDPOINT, data);
      expect(resp.status).toEqual(400);
      expect(resp.data).toMatchObject({
        error_msg:
          'invalid routes at index 0, err: property "uri" validation failed: wrong type: expected string, got number',
      });
    });

    it("update config (empty request body)", async () => {
      const resp = await clientException.put(ENDPOINT, "");
      expect(resp.status).toEqual(400);
      expect(resp.data).toEqual({
        error_msg: "invalid request body: empty request body",
      });
    });
  });
});
