// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ostream>
#include <sstream>
#include <string>
#include <vector>

#include "lib/jpegli/encode.h"
#include "lib/jpegli/libjpeg_test_util.h"
#include "lib/jpegli/test_params.h"
#include "lib/jpegli/test_utils.h"
#include "lib/jpegli/testing.h"
#include "lib/jpegli/types.h"

namespace jpegli {
namespace {

struct TestConfig {
  TestImage input;
  CompressParams jparams;
  JpegIOMode input_mode = PIXELS;
  double max_bpp;
  double max_dist;
};

class EncodeAPITestParam : public ::testing::TestWithParam<TestConfig> {};

void GenerateInput(JpegIOMode input_mode, const CompressParams& jparams,
                   TestImage* input) {
  GeneratePixels(input);
  if (input_mode == RAW_DATA) {
    GenerateRawData(jparams, input);
  } else if (input_mode == COEFFICIENTS) {
    GenerateCoeffs(jparams, input);
  }
}

TEST_P(EncodeAPITestParam, TestAPI) {
  TestConfig config = GetParam();
  GenerateInput(config.input_mode, config.jparams, &config.input);
  std::vector<uint8_t> compressed;
  ASSERT_TRUE(EncodeWithJpegli(config.input, config.jparams, &compressed));
  if (config.jparams.icc.empty()) {
    double bpp =
        compressed.size() * 8.0 / (config.input.xsize * config.input.ysize);
    printf("bpp: %f\n", bpp);
    EXPECT_LT(bpp, config.max_bpp);
  }
  DecompressParams dparams;
  dparams.output_mode =
      config.input_mode == COEFFICIENTS ? COEFFICIENTS : PIXELS;
  if (config.jparams.set_jpeg_colorspace &&
      config.jparams.jpeg_color_space == JCS_GRAYSCALE) {
    ConvertToGrayscale(&config.input);
  } else {
    dparams.set_out_color_space = true;
    dparams.out_color_space = config.input.color_space;
  }
  TestImage output;
  DecodeWithLibjpeg(config.jparams, dparams, compressed, &output);
  VerifyOutputImage(config.input, output, config.max_dist);
}

TEST(EncodeAPITest, ReuseCinfoSameImageTwice) {
  TestImage input;
  input.xsize = 129;
  input.ysize = 73;
  CompressParams jparams;
  GenerateInput(PIXELS, jparams, &input);
  uint8_t* buffer = nullptr;
  unsigned long buffer_size = 0;  // NOLINT
  std::vector<uint8_t> compressed0;
  std::vector<uint8_t> compressed1;
  jpeg_compress_struct cinfo;
  const auto try_catch_block = [&]() -> bool {
    ERROR_HANDLER_SETUP(jpegli);
    jpegli_create_compress(&cinfo);
    jpegli_mem_dest(&cinfo, &buffer, &buffer_size);
    EncodeWithJpegli(input, jparams, &cinfo);
    compressed0.assign(buffer, buffer + buffer_size);
    jpegli_mem_dest(&cinfo, &buffer, &buffer_size);
    EncodeWithJpegli(input, jparams, &cinfo);
    compressed1.assign(buffer, buffer + buffer_size);
    return true;
  };
  EXPECT_TRUE(try_catch_block());
  jpegli_destroy_compress(&cinfo);
  if (buffer) free(buffer);
  ASSERT_EQ(compressed0.size(), compressed1.size());
  EXPECT_EQ(0,
            memcmp(compressed0.data(), compressed1.data(), compressed0.size()));
}

std::vector<TestConfig> GenerateBasicConfigs() {
  std::vector<TestConfig> all_configs;
  for (int samp : {1, 2}) {
    for (int progr : {0, 2}) {
      for (int optimize : {0, 1}) {
        if (progr && optimize) continue;
        TestConfig config;
        config.input.xsize = 257 + samp * 37;
        config.input.ysize = 265 + optimize * 17;
        config.jparams.h_sampling = {samp, 1, 1};
        config.jparams.v_sampling = {samp, 1, 1};
        config.jparams.progressive_mode = progr;
        config.jparams.optimize_coding = optimize;
        config.max_dist = 2.4f;
        GeneratePixels(&config.input);
        all_configs.push_back(config);
      }
    }
  }
  return all_configs;
}

TEST(EncodeAPITest, ReuseCinfoSameMemOutput) {
  std::vector<TestConfig> all_configs = GenerateBasicConfigs();
  uint8_t* buffer = nullptr;
  unsigned long buffer_size = 0;  // NOLINT
  {
    jpeg_compress_struct cinfo;
    const auto try_catch_block = [&]() -> bool {
      ERROR_HANDLER_SETUP(jpegli);
      jpegli_create_compress(&cinfo);
      jpegli_mem_dest(&cinfo, &buffer, &buffer_size);
      for (const TestConfig& config : all_configs) {
        EncodeWithJpegli(config.input, config.jparams, &cinfo);
      }
      return true;
    };
    EXPECT_TRUE(try_catch_block());
    jpegli_destroy_compress(&cinfo);
  }
  size_t pos = 0;
  for (auto& config : all_configs) {
    TestImage output;
    pos += DecodeWithLibjpeg(config.jparams, DecompressParams(), nullptr, 0,
                             buffer + pos, buffer_size - pos, &output);
    VerifyOutputImage(config.input, output, config.max_dist);
  }
  if (buffer) free(buffer);
}

TEST(EncodeAPITest, ReuseCinfoSameStdOutput) {
  std::vector<TestConfig> all_configs = GenerateBasicConfigs();
  FILE* tmpf = tmpfile();
  ASSERT_TRUE(tmpf);
  {
    jpeg_compress_struct cinfo;
    const auto try_catch_block = [&]() -> bool {
      ERROR_HANDLER_SETUP(jpegli);
      jpegli_create_compress(&cinfo);
      jpegli_stdio_dest(&cinfo, tmpf);
      for (const TestConfig& config : all_configs) {
        EncodeWithJpegli(config.input, config.jparams, &cinfo);
      }
      return true;
    };
    EXPECT_TRUE(try_catch_block());
    jpegli_destroy_compress(&cinfo);
  }
  size_t total_size = ftell(tmpf);
  fseek(tmpf, 0, SEEK_SET);
  std::vector<uint8_t> compressed(total_size);
  ASSERT_TRUE(total_size == fread(compressed.data(), 1, total_size, tmpf));
  fclose(tmpf);
  size_t pos = 0;
  for (auto& config : all_configs) {
    TestImage output;
    pos +=
        DecodeWithLibjpeg(config.jparams, DecompressParams(), nullptr, 0,
                          &compressed[pos], compressed.size() - pos, &output);
    VerifyOutputImage(config.input, output, config.max_dist);
  }
}

TEST(EncodeAPITest, ReuseCinfoChangeParams) {
  TestImage input;
  TestImage output;
  CompressParams jparams;
  DecompressParams dparams;
  uint8_t* buffer = nullptr;
  unsigned long buffer_size = 0;  // NOLINT
  std::vector<uint8_t> compressed;
  jpeg_compress_struct cinfo;
  const auto max_rms = [](int q, int hs, int vs) {
    if (hs == 1 && vs == 1) return q == 90 ? 2.2 : 0.6;
    if (hs == 2 && vs == 2) return q == 90 ? 2.8 : 1.2;
    return q == 90 ? 2.4 : 1.0;
  };
  const auto try_catch_block = [&]() -> bool {
    ERROR_HANDLER_SETUP(jpegli);
    jpegli_create_compress(&cinfo);
    input.xsize = 129;
    input.ysize = 73;
    dparams.set_out_color_space = true;
    for (JpegIOMode input_mode : {PIXELS, RAW_DATA, PIXELS, COEFFICIENTS}) {
      for (int h_samp : {2, 1}) {
        for (int v_samp : {2, 1}) {
          for (int progr : {0, 2}) {
            for (int quality : {90, 100}) {
              input.Clear();
              input.color_space =
                  (input_mode == RAW_DATA ? JCS_YCbCr : JCS_RGB);
              jparams.quality = quality;
              jparams.h_sampling = {h_samp, 1, 1};
              jparams.v_sampling = {v_samp, 1, 1};
              jparams.progressive_mode = progr;
              printf(
                  "Generating input with quality %d chroma subsampling %dx%d "
                  "input mode %d progressive_mode %d\n",
                  quality, h_samp, v_samp, input_mode, progr);
              GenerateInput(input_mode, jparams, &input);
              jpegli_mem_dest(&cinfo, &buffer, &buffer_size);
              if (input_mode != COEFFICIENTS) {
                cinfo.image_width = input.xsize;
                cinfo.image_height = input.ysize;
                cinfo.input_components = input.components;
                jpegli_set_defaults(&cinfo);
                jpegli_start_compress(&cinfo, TRUE);
                jpegli_abort_compress(&cinfo);
                jpegli_mem_dest(&cinfo, &buffer, &buffer_size);
              }
              EncodeWithJpegli(input, jparams, &cinfo);
              compressed.resize(buffer_size);
              std::copy_n(buffer, buffer_size, compressed.data());
              dparams.output_mode =
                  input_mode == COEFFICIENTS ? COEFFICIENTS : PIXELS;
              dparams.out_color_space = input.color_space;
              output.Clear();
              DecodeWithLibjpeg(jparams, dparams, compressed, &output);
              VerifyOutputImage(input, output,
                                max_rms(quality, h_samp, v_samp));
            }
          }
        }
      }
    }
    return true;
  };
  EXPECT_TRUE(try_catch_block());
  jpegli_destroy_compress(&cinfo);
  if (buffer) free(buffer);
}

TEST(EncodeAPITest, AbbreviatedStreams) {
  uint8_t* table_stream = nullptr;
  unsigned long table_stream_size = 0;  // NOLINT
  uint8_t* data_stream = nullptr;
  unsigned long data_stream_size = 0;  // NOLINT
  {
    jpeg_compress_struct cinfo;
    const auto try_catch_block = [&]() -> bool {
      ERROR_HANDLER_SETUP(jpegli);
      jpegli_create_compress(&cinfo);
      jpegli_mem_dest(&cinfo, &table_stream, &table_stream_size);
      cinfo.input_components = 3;
      cinfo.in_color_space = JCS_RGB;
      jpegli_set_defaults(&cinfo);
      jpegli_write_tables(&cinfo);
      jpegli_mem_dest(&cinfo, &data_stream, &data_stream_size);
      cinfo.image_width = 1;
      cinfo.image_height = 1;
      cinfo.optimize_coding = FALSE;
      jpegli_set_progressive_level(&cinfo, 0);
      jpegli_start_compress(&cinfo, FALSE);
      JSAMPLE image[3] = {0};
      JSAMPROW row[] = {image};
      jpegli_write_scanlines(&cinfo, row, 1);
      jpegli_finish_compress(&cinfo);
      return true;
    };
    EXPECT_TRUE(try_catch_block());
    EXPECT_LT(data_stream_size, 50);
    jpegli_destroy_compress(&cinfo);
  }
  TestImage output;
  DecodeWithLibjpeg(CompressParams(), DecompressParams(), table_stream,
                    table_stream_size, data_stream, data_stream_size, &output);
  EXPECT_EQ(1, output.xsize);
  EXPECT_EQ(1, output.ysize);
  EXPECT_EQ(3, output.components);
  EXPECT_EQ(0, output.pixels[0]);
  EXPECT_EQ(0, output.pixels[1]);
  EXPECT_EQ(0, output.pixels[2]);
  if (table_stream) free(table_stream);
  if (data_stream) free(data_stream);
}

void CopyQuantTables(j_compress_ptr cinfo, uint16_t* quant_tables) {
  for (int c = 0; c < cinfo->num_components; ++c) {
    int quant_idx = cinfo->comp_info[c].quant_tbl_no;
    JQUANT_TBL* quant_table = cinfo->quant_tbl_ptrs[quant_idx];
    for (int k = 0; k < DCTSIZE2; ++k) {
      quant_tables[c * DCTSIZE2 + k] = quant_table->quantval[k];
    }
  }
}

TEST(EncodeAPITest, QualitySettings) {
  // Test that jpegli_set_quality, jpegli_set_linear_quality and
  // jpegli_quality_scaling are consistent with each other.
  uint16_t quant_tables0[3 * DCTSIZE2];
  uint16_t quant_tables1[3 * DCTSIZE2];
  jpeg_compress_struct cinfo;
  const auto try_catch_block = [&]() -> bool {
    ERROR_HANDLER_SETUP(jpegli);
    jpegli_create_compress(&cinfo);
    cinfo.input_components = 3;
    cinfo.in_color_space = JCS_RGB;
    jpegli_set_defaults(&cinfo);
    for (boolean baseline : {FALSE, TRUE}) {
      for (int q = 1; q <= 100; ++q) {
        jpegli_set_quality(&cinfo, q, baseline);
        CopyQuantTables(&cinfo, quant_tables0);
        jpegli_set_linear_quality(&cinfo, jpegli_quality_scaling(q), baseline);
        CopyQuantTables(&cinfo, quant_tables1);
        EXPECT_EQ(0,
                  memcmp(quant_tables0, quant_tables1, sizeof(quant_tables0)));
#if JPEG_LIB_VERSION >= 70
        for (int i = 0; i < NUM_QUANT_TBLS; ++i) {
          cinfo.q_scale_factor[i] = jpegli_quality_scaling(q);
        }
        jpegli_default_qtables(&cinfo, baseline);
        CopyQuantTables(&cinfo, quant_tables1);
        EXPECT_EQ(0,
                  memcmp(quant_tables0, quant_tables1, sizeof(quant_tables0)));
#endif
      }
    }
    return true;
  };
  EXPECT_TRUE(try_catch_block());
  jpegli_destroy_compress(&cinfo);
  // Test jpegli_quality_scaling for some specific values .
  EXPECT_EQ(5000, jpegli_quality_scaling(-1));
  EXPECT_EQ(5000, jpegli_quality_scaling(0));
  EXPECT_EQ(5000, jpegli_quality_scaling(1));
  EXPECT_EQ(100, jpegli_quality_scaling(50));
  EXPECT_EQ(50, jpegli_quality_scaling(75));
  EXPECT_EQ(20, jpegli_quality_scaling(90));
  EXPECT_EQ(0, jpegli_quality_scaling(100));
  EXPECT_EQ(0, jpegli_quality_scaling(101));
}

std::vector<TestConfig> GenerateTests() {
  std::vector<TestConfig> all_tests;
  for (int h_samp : {1, 2}) {
    for (int v_samp : {1, 2}) {
      for (int progr : {0, 2}) {
        for (int optimize : {0, 1}) {
          if (progr && optimize) continue;
          TestConfig config;
          config.jparams.h_sampling = {h_samp, 1, 1};
          config.jparams.v_sampling = {v_samp, 1, 1};
          config.jparams.progressive_mode = progr;
          if (!progr) {
            config.jparams.optimize_coding = optimize;
          }
          const float kMaxBpp[4] = {1.55, 1.4, 1.4, 1.32};
          const float kMaxDist[4] = {1.95, 2.2, 2.2, 2.0};
          const int idx = v_samp * 2 + h_samp - 3;
          config.max_bpp =
              kMaxBpp[idx] * (optimize ? 0.97 : 1.0) * (progr ? 0.97 : 1.0);
          config.max_dist = kMaxDist[idx];
          all_tests.push_back(config);
        }
      }
    }
  }
  {
    TestConfig config;
    config.jparams.quality = 100;
    config.jparams.h_sampling = {1, 1, 1};
    config.jparams.v_sampling = {1, 1, 1};
    config.max_bpp = 6.6;
    config.max_dist = 0.6;
    all_tests.push_back(config);
  }
  {
    TestConfig config;
    config.jparams.quality = 80;
    config.max_bpp = 1.05;
    config.max_dist = 2.7;
    all_tests.push_back(config);
  }
  for (int samp : {1, 2}) {
    for (int progr : {0, 2}) {
      for (int optimize : {0, 1}) {
        if (progr && optimize) continue;
        TestConfig config;
        config.input.xsize = 257;
        config.input.ysize = 265;
        config.jparams.h_sampling = {samp, 1, 1};
        config.jparams.v_sampling = {samp, 1, 1};
        config.jparams.progressive_mode = progr;
        if (!progr) {
          config.jparams.optimize_coding = optimize;
        }
        config.jparams.use_adaptive_quantization = false;
        config.max_bpp = 2.05f;
        config.max_dist = 2.3f;
        all_tests.push_back(config);
      }
    }
  }
  for (int h0_samp : {1, 2, 4}) {
    for (int v0_samp : {1, 2, 4}) {
      for (int h2_samp : {1, 2, 4}) {
        for (int v2_samp : {1, 2, 4}) {
          TestConfig config;
          config.input.xsize = 137;
          config.input.ysize = 75;
          config.jparams.progressive_mode = 2;
          config.jparams.h_sampling = {h0_samp, 1, h2_samp};
          config.jparams.v_sampling = {v0_samp, 1, v2_samp};
          config.max_bpp = 2.5;
          config.max_dist = 12.0;
          all_tests.push_back(config);
        }
      }
    }
  }
  for (int h0_samp : {1, 3}) {
    for (int v0_samp : {1, 3}) {
      for (int h2_samp : {1, 3}) {
        for (int v2_samp : {1, 3}) {
          TestConfig config;
          config.input.xsize = 205;
          config.input.ysize = 99;
          config.jparams.progressive_mode = 2;
          config.jparams.h_sampling = {h0_samp, 1, h2_samp};
          config.jparams.v_sampling = {v0_samp, 1, v2_samp};
          config.max_bpp = 2.5;
          config.max_dist = 10.0;
          all_tests.push_back(config);
        }
      }
    }
  }
  for (int h0_samp : {1, 2, 3, 4}) {
    for (int v0_samp : {1, 2, 3, 4}) {
      TestConfig config;
      config.input.xsize = 217;
      config.input.ysize = 129;
      config.jparams.progressive_mode = 2;
      config.jparams.h_sampling = {h0_samp, 1, 1};
      config.jparams.v_sampling = {v0_samp, 1, 1};
      config.max_bpp = 2.0;
      config.max_dist = 5.5;
      all_tests.push_back(config);
    }
  }
  for (int p = 0; p < 3 + NumTestScanScripts(); ++p) {
    for (int samp : {1, 2}) {
      for (int quality : {100, 90, 1}) {
        for (int r : {0, 1024, 1}) {
          for (int optimize : {0, 1}) {
            bool progressive = p == 1 || p == 2 || p > 4;
            if (progressive && !optimize) continue;
            TestConfig config;
            config.input.xsize = 273;
            config.input.ysize = 265;
            config.jparams.progressive_mode = p;
            if (!progressive) {
              config.jparams.optimize_coding = optimize;
            }
            config.jparams.h_sampling = {samp, 1, 1};
            config.jparams.v_sampling = {samp, 1, 1};
            config.jparams.quality = quality;
            config.jparams.restart_interval = r;
            config.max_bpp = quality == 100 ? 8.0 : 1.9;
            if (r == 1) {
              config.max_bpp += 10.0;
            }
            config.max_dist = quality == 1 ? 20.0 : 2.1;
            all_tests.push_back(config);
          }
        }
      }
    }
  }
  {
    TestConfig config;
    config.jparams.simple_progression = true;
    config.max_bpp = 1.48;
    config.max_dist = 2.0;
    all_tests.push_back(config);
  }
  {
    TestConfig config;
    config.input_mode = COEFFICIENTS;
    config.jparams.h_sampling = {2, 1, 1};
    config.jparams.v_sampling = {2, 1, 1};
    config.jparams.progressive_mode = 0;
    config.jparams.optimize_coding = 0;
    config.max_bpp = 16;
    config.max_dist = 0.0;
    all_tests.push_back(config);
  }
  {
    TestConfig config;
    config.jparams.xyb_mode = true;
    config.jparams.progressive_mode = 2;
    config.max_bpp = 1.5;
    config.max_dist = 3.5;
    all_tests.push_back(config);
  }
  {
    TestConfig config;
    config.jparams.libjpeg_mode = true;
    config.max_bpp = 2.1;
    config.max_dist = 1.7;
    config.jparams.h_sampling = {1, 1, 1};
    config.jparams.v_sampling = {1, 1, 1};
    all_tests.push_back(config);
  }

  for (J_COLOR_SPACE in_color_space :
       {JCS_RGB, JCS_YCbCr, JCS_GRAYSCALE, JCS_EXT_RGB, JCS_EXT_BGR,
        JCS_EXT_RGBA, JCS_EXT_BGRA, JCS_EXT_ARGB, JCS_EXT_ABGR}) {
    for (J_COLOR_SPACE jpeg_color_space : {JCS_RGB, JCS_YCbCr, JCS_GRAYSCALE}) {
      if (jpeg_color_space == JCS_RGB && in_color_space >= JCS_YCbCr) continue;
      TestConfig config;
      config.input.xsize = config.input.ysize = 256;
      config.input.color_space = in_color_space;
      config.jparams.set_jpeg_colorspace = true;
      config.jparams.jpeg_color_space = jpeg_color_space;
      config.jparams.h_sampling = {1, 1, 1};
      config.jparams.v_sampling = {1, 1, 1};
      config.max_bpp = jpeg_color_space == JCS_RGB ? 4.5 : 1.85;
      config.max_dist = jpeg_color_space == JCS_RGB ? 1.4 : 2.05;
      all_tests.push_back(config);
    }
  }
  for (J_COLOR_SPACE in_color_space : {JCS_CMYK, JCS_YCCK}) {
    for (J_COLOR_SPACE jpeg_color_space : {JCS_CMYK, JCS_YCCK}) {
      if (jpeg_color_space == JCS_CMYK && in_color_space == JCS_YCCK) continue;
      TestConfig config;
      config.input.xsize = config.input.ysize = 256;
      config.input.color_space = in_color_space;
      if (in_color_space != jpeg_color_space) {
        config.jparams.set_jpeg_colorspace = true;
        config.jparams.jpeg_color_space = jpeg_color_space;
      }
      config.jparams.h_sampling = {1, 1, 1, 1};
      config.jparams.v_sampling = {1, 1, 1, 1};
      config.max_bpp = jpeg_color_space == JCS_CMYK ? 4.0 : 3.6;
      config.max_dist = jpeg_color_space == JCS_CMYK ? 1.2 : 1.5;
      all_tests.push_back(config);
    }
  }
  {
    TestConfig config;
    config.input.color_space = JCS_YCbCr;
    config.max_bpp = 1.6;
    config.max_dist = 1.35;
    config.jparams.h_sampling = {1, 1, 1};
    config.jparams.v_sampling = {1, 1, 1};
    all_tests.push_back(config);
  }
  for (bool xyb : {false, true}) {
    TestConfig config;
    config.input.color_space = JCS_GRAYSCALE;
    config.jparams.xyb_mode = xyb;
    config.max_bpp = 1.35;
    config.max_dist = 1.4;
    all_tests.push_back(config);
  }
  for (int channels = 1; channels <= 4; ++channels) {
    TestConfig config;
    config.input.color_space = JCS_UNKNOWN;
    config.input.components = channels;
    config.max_bpp = 1.35 * channels;
    config.max_dist = 1.4;
    all_tests.push_back(config);
  }
  for (size_t r : {1, 3, 17, 1024}) {
    for (int progr : {0, 2}) {
      TestConfig config;
      config.jparams.restart_interval = r;
      config.jparams.progressive_mode = progr;
      config.max_bpp = 1.58 + 5.5 / r;
      config.max_dist = 2.2;
      all_tests.push_back(config);
    }
  }
  for (size_t rr : {1, 3, 8, 100}) {
    TestConfig config;
    config.jparams.restart_in_rows = rr;
    config.max_bpp = 1.6;
    config.max_dist = 2.2;
    all_tests.push_back(config);
  }
  for (int type : {0, 1, 10, 100, 10000}) {
    for (int scale : {1, 50, 100, 200, 500}) {
      for (bool add_raw : {false, true}) {
        for (bool baseline : {true, false}) {
          if (!baseline && (add_raw || type * scale < 25500)) continue;
          TestConfig config;
          config.input.xsize = 64;
          config.input.ysize = 64;
          CustomQuantTable table;
          table.table_type = type;
          table.scale_factor = scale;
          table.force_baseline = baseline;
          table.add_raw = add_raw;
          table.Generate();
          config.jparams.optimize_coding = 1;
          config.jparams.h_sampling = {1, 1, 1};
          config.jparams.v_sampling = {1, 1, 1};
          config.jparams.quant_tables.push_back(table);
          config.jparams.quant_indexes = {0, 0, 0};
          float q = (type == 0 ? 16 : type) * scale * 0.01f;
          if (baseline && !add_raw) q = std::max(1.0f, std::min(255.0f, q));
          config.max_bpp = 1.5f + 25.0f / q;
          config.max_dist = 0.6f + 0.25f * q;
          all_tests.push_back(config);
        }
      }
    }
  }
  for (int qidx = 0; qidx < 8; ++qidx) {
    if (qidx == 3) continue;
    TestConfig config;
    config.input.xsize = 256;
    config.input.ysize = 256;
    config.jparams.quant_indexes = {(qidx >> 2) & 1, (qidx >> 1) & 1,
                                    (qidx >> 0) & 1};
    config.jparams.h_sampling = {1, 1, 1};
    config.jparams.v_sampling = {1, 1, 1};
    config.max_bpp = 2.25;
    config.max_dist = 2.8;
    all_tests.push_back(config);
  }
  for (int qidx = 0; qidx < 8; ++qidx) {
    for (int slot_idx = 0; slot_idx < 2; ++slot_idx) {
      if (qidx == 0 && slot_idx == 0) continue;
      TestConfig config;
      config.input.xsize = 256;
      config.input.ysize = 256;
      config.jparams.quant_indexes = {(qidx >> 2) & 1, (qidx >> 1) & 1,
                                      (qidx >> 0) & 1};
      config.jparams.h_sampling = {1, 1, 1};
      config.jparams.v_sampling = {1, 1, 1};
      CustomQuantTable table;
      table.slot_idx = slot_idx;
      table.Generate();
      config.jparams.quant_tables.push_back(table);
      config.max_bpp = 2.3;
      config.max_dist = 2.9;
      all_tests.push_back(config);
    }
  }
  for (int qidx = 0; qidx < 8; ++qidx) {
    for (bool xyb : {false, true}) {
      TestConfig config;
      config.input.xsize = 256;
      config.input.ysize = 256;
      config.jparams.xyb_mode = xyb;
      config.jparams.quant_indexes = {(qidx >> 2) & 1, (qidx >> 1) & 1,
                                      (qidx >> 0) & 1};
      if (!xyb) {
        config.jparams.h_sampling = {1, 1, 1};
        config.jparams.v_sampling = {1, 1, 1};
      }
      {
        CustomQuantTable table;
        table.slot_idx = 0;
        table.Generate();
        config.jparams.quant_tables.push_back(table);
      }
      {
        CustomQuantTable table;
        table.slot_idx = 1;
        table.table_type = 20;
        table.Generate();
        config.jparams.quant_tables.push_back(table);
      }
      config.max_bpp = 2.0;
      config.max_dist = 3.85;
      all_tests.push_back(config);
    }
  }
  for (bool xyb : {false, true}) {
    TestConfig config;
    config.input.xsize = 256;
    config.input.ysize = 256;
    config.jparams.xyb_mode = xyb;
    config.jparams.quant_indexes = {0, 1, 2};
    if (!xyb) {
      config.jparams.h_sampling = {1, 1, 1};
      config.jparams.v_sampling = {1, 1, 1};
    }
    {
      CustomQuantTable table;
      table.slot_idx = 0;
      table.Generate();
      config.jparams.quant_tables.push_back(table);
    }
    {
      CustomQuantTable table;
      table.slot_idx = 1;
      table.table_type = 20;
      table.Generate();
      config.jparams.quant_tables.push_back(table);
    }
    {
      CustomQuantTable table;
      table.slot_idx = 2;
      table.table_type = 30;
      table.Generate();
      config.jparams.quant_tables.push_back(table);
    }
    config.max_bpp = 1.5;
    config.max_dist = 3.75;
    all_tests.push_back(config);
  }
  {
    TestConfig config;
    config.jparams.comp_ids = {7, 17, 177};
    config.input.xsize = config.input.ysize = 128;
    config.max_bpp = 2.25;
    config.max_dist = 2.4;
    all_tests.push_back(config);
  }
  for (int override_JFIF : {-1, 0, 1}) {
    for (int override_Adobe : {-1, 0, 1}) {
      if (override_JFIF == -1 && override_Adobe == -1) continue;
      TestConfig config;
      config.input.xsize = config.input.ysize = 128;
      config.jparams.override_JFIF = override_JFIF;
      config.jparams.override_Adobe = override_Adobe;
      config.max_bpp = 2.25;
      config.max_dist = 2.4;
      all_tests.push_back(config);
    }
  }
  {
    TestConfig config;
    config.input.xsize = config.input.ysize = 256;
    config.max_bpp = 1.85;
    config.max_dist = 2.05;
    config.jparams.add_marker = true;
    all_tests.push_back(config);
  }
  for (size_t icc_size : {728, 70000, 1000000}) {
    TestConfig config;
    config.input.xsize = config.input.ysize = 256;
    config.max_dist = 2.05;
    config.jparams.icc.resize(icc_size);
    for (size_t i = 0; i < icc_size; ++i) {
      config.jparams.icc[i] = (i * 17) & 0xff;
    }
    all_tests.push_back(config);
  }
  for (JpegIOMode input_mode : {PIXELS, RAW_DATA, COEFFICIENTS}) {
    TestConfig config;
    config.input.xsize = config.input.ysize = 256;
    config.input_mode = input_mode;
    if (input_mode == RAW_DATA) {
      config.input.color_space = JCS_YCbCr;
    }
    config.jparams.progressive_mode = 0;
    config.jparams.optimize_coding = 0;
    config.jparams.h_sampling = {1, 1, 1};
    config.jparams.v_sampling = {1, 1, 1};
    config.max_bpp = 1.85;
    config.max_dist = 2.05;
    if (input_mode == COEFFICIENTS) {
      config.max_bpp = 3.5;
      config.max_dist = 0.0;
    }
    all_tests.push_back(config);
    config.jparams.use_flat_dc_luma_code = true;
    all_tests.push_back(config);
  }
  for (int xsize : {640, 641, 648, 649}) {
    for (int ysize : {640, 641, 648, 649}) {
      for (int h_sampling : {1, 2}) {
        for (int v_sampling : {1, 2}) {
          if (h_sampling == 1 && v_sampling == 1) continue;
          for (int progr : {0, 2}) {
            TestConfig config;
            config.input.xsize = xsize;
            config.input.ysize = ysize;
            config.input.color_space = JCS_YCbCr;
            config.jparams.h_sampling = {h_sampling, 1, 1};
            config.jparams.v_sampling = {v_sampling, 1, 1};
            config.jparams.progressive_mode = progr;
            config.input_mode = RAW_DATA;
            config.max_bpp = 1.75;
            config.max_dist = 2.0;
            all_tests.push_back(config);
            config.input_mode = COEFFICIENTS;
            if (xsize & 1) {
              config.jparams.add_marker = true;
            }
            config.max_bpp = 24.0;
            all_tests.push_back(config);
          }
        }
      }
    }
  }
  for (JpegliDataType data_type : {JPEGLI_TYPE_UINT16, JPEGLI_TYPE_FLOAT}) {
    for (JpegliEndianness endianness :
         {JPEGLI_LITTLE_ENDIAN, JPEGLI_BIG_ENDIAN, JPEGLI_NATIVE_ENDIAN}) {
      J_COLOR_SPACE colorspace[4] = {JCS_GRAYSCALE, JCS_UNKNOWN, JCS_RGB,
                                     JCS_CMYK};
      float max_bpp[4] = {1.32, 2.7, 1.6, 4.0};
      for (int channels = 1; channels <= 4; ++channels) {
        TestConfig config;
        config.input.data_type = data_type;
        config.input.endianness = endianness;
        config.input.components = channels;
        config.input.color_space = colorspace[channels - 1];
        config.max_bpp = max_bpp[channels - 1];
        config.max_dist = 2.2;
        all_tests.push_back(config);
      }
    }
  }
  for (int smoothing : {1, 5, 50, 100}) {
    for (int h_sampling : {1, 2}) {
      for (int v_sampling : {1, 2}) {
        TestConfig config;
        config.input.xsize = 257;
        config.input.ysize = 265;
        config.jparams.smoothing_factor = smoothing;
        config.jparams.h_sampling = {h_sampling, 1, 1};
        config.jparams.v_sampling = {v_sampling, 1, 1};
        config.max_bpp = 1.85;
        config.max_dist = 3.05f;
        all_tests.push_back(config);
      }
    }
  }
  return all_tests;
};

std::ostream& operator<<(std::ostream& os, const TestConfig& c) {
  os << c.input;
  os << c.jparams;
  if (c.input_mode == RAW_DATA) {
    os << "RawDataIn";
  } else if (c.input_mode == COEFFICIENTS) {
    os << "WriteCoeffs";
  }
  return os;
}

std::string TestDescription(
    const testing::TestParamInfo<EncodeAPITestParam::ParamType>& info) {
  std::stringstream name;
  name << info.param;
  return name.str();
}

JPEGLI_INSTANTIATE_TEST_SUITE_P(EncodeAPITest, EncodeAPITestParam,
                                testing::ValuesIn(GenerateTests()),
                                TestDescription);
}  // namespace
}  // namespace jpegli
