/**
 * Copyright IBM Corp. 2024, 2025
 */

import { z } from 'zod';
import {
  BaseModel,
  IfConditionSchema,
  RequestSkippedSchema,
  StopOnFailSchema,
} from './shared.schema.js';
import { EnvironmentSchema } from './environment.schema.js';
import { AssertionSchema } from './assertions.schema.js';

const refRule = z
  .string()
  .refine((val) => val !== '', {
    message: '$ref cannot be an empty string',
  })
  .optional();

const apiRefRule = z
  .union([
    z.string().refine((val) => val !== '', {
      message: '$ref cannot be an empty string',
    }),
    z.array(
      z.string().refine((val) => val !== '', {
        message: '$ref array items cannot be empty strings',
      }),
    ),
  ])
  .optional();

const RawPayloadSchema = z
  .object({
    json: z.string().optional(),
    js: z.string().optional(),
    html: z.string().optional(),
    xml: z.string().optional(),
  })
  .strict()
  .optional();

const KeyValueSchema = z
  .array(
    z.object({
      key: z.string(),
      value: z.any().refine((val) => val !== undefined, {
        message: 'value is required',
      }),
      type: z.string().optional(),
    }),
  )
  .optional();

export const PayloadUnionSchema = z
  .object({
    raw: RawPayloadSchema,
    urlEncodedFormData: KeyValueSchema,
    formData: KeyValueSchema,
  })
  .refine(
    (data) => {
      const keys = ['raw', 'urlEncodedFormData', 'formData'];
      const presentKeys = keys.filter(
        (key) => data[key as keyof typeof data] !== undefined,
      );
      return presentKeys.length === 1;
    },
    {
      message:
        'Exactly one of raw, urlEncodedFormData, or formData must be provided in payload',
    },
  );

const ApiRefOrEndpointSchema = z
  .object({
    $ref: apiRefRule,
    $endpoint: z.string().optional(),
  })
  .refine(
    (data) => (data.$ref && !data.$endpoint) || (!data.$ref && data.$endpoint),
    {
      message: 'Either $ref or $endpoint must be provided, but not both in api',
      path: ['$ref', '$endpoint'],
    },
  );

const AssertionRefSchema = z
  .object({
    $ref: refRule,
    assertions: z.array(AssertionSchema).optional(),
  })
  .refine(
    (data) =>
      data == undefined ||
      data.$ref !== undefined ||
      (data.assertions !== undefined && data.assertions.length > 0),
    {
      message:
        'Either $ref or assertions (non-empty array) with complete data must be provided',
      path: ['$ref', 'assertions'],
    },
  );

const EnvironmentSpecSchema = z
  .object({
    $ref: refRule,
    variables: z.array(EnvironmentSchema).optional(),
  })
  .refine(
    (data) =>
      data == undefined ||
      data.$ref !== undefined ||
      (data.variables !== undefined && data.variables.length > 0),
    {
      message:
        'Either $ref or variables (non-empty array) must be provided in environment',
      path: ['$ref', 'variables'],
    },
  );

const BasicAuthSchema = z.object({
  username: z.string(),
  password: z.string(),
});

export const AuthSchema = z
  .object({
    noauth: z.boolean().optional(),
    bearerToken: z.string().optional(),
    basicAuth: BasicAuthSchema.optional(),
  })
  .refine(
    (data) => {
      const keys = ['noauth', 'bearerToken', 'basicAuth'];
      const present = keys.filter(
        (key) => data[key as keyof typeof data] !== undefined,
      );
      return present.length <= 1;
    },
    {
      message:
        'Only one of noauth, bearerToken, or basicAuth must be provided in auth',
      path: ['auth'],
    },
  );

export const TestStepSchema = z.object({
  endpoint: z.string().optional(),
  method: z.string(),
  if: IfConditionSchema.optional(),
  stopOnFail: StopOnFailSchema.optional(),
  skipped: RequestSkippedSchema.optional(),
  resource: z.string(),
  headers: z
    .array(
      z.object({
        key: z.string(),
        value: z.any().refine((val) => val !== undefined, {
          message: 'value is required in headers',
        }),
        description: z.string().optional(),
      }),
    )
    .optional(),
  auth: AuthSchema.optional(),
  payload: PayloadUnionSchema.optional(),
  settings: z
    .object({
      sslVerification: z.boolean().optional(),
      encodeURL: z.boolean().optional(),
    })
    .optional(),
  parameters: KeyValueSchema.optional(),
  assertions: z
    .union([
      // New format: array of objects with $ref
      z.array(AssertionRefSchema.optional()),
      // Single assertion with direct $ref property
      AssertionRefSchema,
    ])
    .optional(),
  var: z
    .union([
      z.string(),
      z.array(
        z.union([
          z.record(z.string(), z.string()),
          z.object({
            key: z.string(),
            value: z.string(),
          }),
        ]),
      ),
    ])
    .optional(),
});

export const TestSchema = BaseModel.extend({
  kind: z.literal('test'),
  spec: z.object({
    // will be the url to make the request
    api: ApiRefOrEndpointSchema,
    environment: z
      .union([
        // New format: array of objects with $ref
        z.array(EnvironmentSpecSchema.optional()),
        // Single assertion with direct $ref property
        EnvironmentSpecSchema,
      ])
      .optional(),
    // tests which will have path and assertions
    request: z.array(TestStepSchema),
  }),
  vcmId: z.string().optional(),
});

export type Test = z.infer<typeof TestSchema>;

export type Request = z.infer<typeof TestStepSchema>;

export type Payload = z.infer<typeof PayloadUnionSchema>;

export type AuthOptions = z.infer<typeof AuthSchema>;

export type Assertions = z.infer<typeof AssertionRefSchema>;
