// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/services/readaloud/audio_renderer/read_aloud_audio_renderer.h"

#include <memory>

#include "chrome/services/readaloud/audio_segment_queue.h"
#include "chrome/services/readaloud/decoded_audio_segment.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_parameters.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace readaloud {

class ReadAloudAudioRendererTest : public testing::Test {
 protected:
  void SetUp() override {
    queue_ = std::make_unique<AudioSegmentQueue>();
    renderer_ = std::make_unique<ReadAloudAudioRenderer>();
  }

  scoped_refptr<DecodedAudioSegment> GenerateSegment(
      const media::AudioParameters& params,
      int frames,
      float base_value = 0.0f,
      float step = 0.001f) {
    auto bus = media::AudioBus::Create(params.channels(), frames);
    for (int i = 0; i < frames; ++i) {
      float val = base_value + i * step;
      if (bus->channels() > 0) {
        bus->channel(0)[i] = val;
      }
      if (bus->channels() > 1) {
        bus->channel(1)[i] = -val;
      }
    }
    auto buffer = media::AudioBuffer::CopyFrom(params.sample_rate(),
                                               base::TimeDelta(), bus.get());
    return base::MakeRefCounted<DecodedAudioSegment>(std::move(buffer));
  }

  // Verifies that a range of the destination AudioBus matches the expected
  // pattern generated by `GenerateSegment` (accounting for frame offsets).
  void VerifySegment(const media::AudioBus& bus,
                     int start_frame,
                     int end_frame,
                     int start_frame_offset,
                     float base_value = 0.0f,
                     float step = 0.001f) {
    for (int i = start_frame; i < end_frame; ++i) {
      float expected_val =
          base_value + (i - start_frame + start_frame_offset) * step;
      if (bus.channels() > 0) {
        ASSERT_NEAR(bus.channel(0)[i], expected_val, 1e-5f)
            << "Left channel mismatch at frame " << i;
      }
      if (bus.channels() > 1) {
        ASSERT_NEAR(bus.channel(1)[i], -expected_val, 1e-5f)
            << "Right channel mismatch at frame " << i;
      }
    }
  }

  std::unique_ptr<AudioSegmentQueue> queue_;
  std::unique_ptr<ReadAloudAudioRenderer> renderer_;
};

TEST_F(ReadAloudAudioRendererTest, LifecycleInitializeValid) {
  media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                media::ChannelLayoutConfig::Stereo(), 48000,
                                480);
  EXPECT_TRUE(params.IsValid());
  EXPECT_TRUE(renderer_->Initialize(params, queue_.get()));
}

TEST_F(ReadAloudAudioRendererTest, LifecycleInitializeInvalid) {
  // Invalid sample rate
  media::AudioParameters invalid_params(
      media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
      media::ChannelLayoutConfig::Stereo(), 0, 480);
  EXPECT_FALSE(invalid_params.IsValid());
  EXPECT_FALSE(renderer_->Initialize(invalid_params, queue_.get()));

  // Null queue
  media::AudioParameters valid_params(
      media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
      media::ChannelLayoutConfig::Stereo(), 48000, 480);
  EXPECT_TRUE(valid_params.IsValid());
  EXPECT_FALSE(renderer_->Initialize(valid_params, nullptr));
}

TEST_F(ReadAloudAudioRendererTest, RenderReturnsSilenceWhenQueueIsEmpty) {
  media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                media::ChannelLayoutConfig::Stereo(), 48000,
                                480);
  ASSERT_TRUE(renderer_->Initialize(params, queue_.get()));

  auto dest = media::AudioBus::Create(params);
  // Pre-fill destination with non-zero values to ensure we can verify it gets
  // zeroed.
  for (int c = 0; c < dest->channels(); ++c) {
    for (int i = 0; i < dest->frames(); ++i) {
      dest->channel(c)[i] = 1.0f;
    }
  }

  int frames_rendered = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest.get());

  EXPECT_EQ(frames_rendered, 0);

  // Verify the destination buffer was zeroed out.
  EXPECT_TRUE(dest->AreFramesZero());
}

TEST_F(ReadAloudAudioRendererTest, RenderWithoutInitializeZeroesBuffer) {
  media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                media::ChannelLayoutConfig::Stereo(), 48000,
                                480);
  auto dest = media::AudioBus::Create(params);
  // Pre-fill destination with non-zero values.
  for (int c = 0; c < dest->channels(); ++c) {
    for (int i = 0; i < dest->frames(); ++i) {
      dest->channel(c)[i] = 1.0f;
    }
  }

  int frames_rendered = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest.get());

  EXPECT_EQ(frames_rendered, 0);

  // Verify the destination buffer was zeroed out because it is not initialized.
  EXPECT_TRUE(dest->AreFramesZero());
}

TEST_F(ReadAloudAudioRendererTest, RenderMatchesSegmentSize) {
  media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                media::ChannelLayoutConfig::Stereo(), 48000,
                                480);
  ASSERT_TRUE(renderer_->Initialize(params, queue_.get()));

  // Create a segment of 480 frames.
  auto segment = GenerateSegment(params, /*frames=*/480);
  ASSERT_TRUE(queue_->Push(segment));

  auto dest = media::AudioBus::Create(params);
  int frames_rendered = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest.get());

  EXPECT_EQ(frames_rendered, 480);
  VerifySegment(*dest, /*start_frame=*/0, /*end_frame=*/dest->frames(),
                /*start_frame_offset=*/0);
}

TEST_F(ReadAloudAudioRendererTest, RenderPartialCopies) {
  media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                media::ChannelLayoutConfig::Stereo(), 48000,
                                480);
  ASSERT_TRUE(renderer_->Initialize(params, queue_.get()));

  // Create a segment of 960 frames.
  auto segment = GenerateSegment(params, /*frames=*/960);
  ASSERT_TRUE(queue_->Push(segment));

  // Render first half
  auto dest1 = media::AudioBus::Create(params);
  int frames_rendered_1 = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest1.get());
  EXPECT_EQ(frames_rendered_1, 480);
  VerifySegment(*dest1, /*start_frame=*/0, /*end_frame=*/dest1->frames(),
                /*start_frame_offset=*/0);

  // Render second half
  auto dest2 = media::AudioBus::Create(params);
  int frames_rendered_2 = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest2.get());
  EXPECT_EQ(frames_rendered_2, 480);
  VerifySegment(*dest2, /*start_frame=*/0, /*end_frame=*/dest2->frames(),
                /*start_frame_offset=*/480);
}

TEST_F(ReadAloudAudioRendererTest, RenderMultipleSegments) {
  media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                media::ChannelLayoutConfig::Stereo(), 48000,
                                480);
  ASSERT_TRUE(renderer_->Initialize(params, queue_.get()));

  // Create segment 1 of 200 frames.
  auto segment1 = GenerateSegment(params, /*frames=*/200, /*base_value=*/0.1f);
  ASSERT_TRUE(queue_->Push(segment1));

  // Create segment 2 of 200 frames.
  auto segment2 = GenerateSegment(params, /*frames=*/200, /*base_value=*/0.5f);
  ASSERT_TRUE(queue_->Push(segment2));

  // Destination of 480 frames, pre-filled with -1.0f.
  auto dest = media::AudioBus::Create(params);
  for (int c = 0; c < dest->channels(); ++c) {
    for (int i = 0; i < dest->frames(); ++i) {
      dest->channel(c)[i] = -1.0f;
    }
  }

  int frames_rendered = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest.get());

  // We only had 400 frames available across both segments.
  EXPECT_EQ(frames_rendered, 400);

  // Verify segment 1, segment 2, and silence ranges.
  VerifySegment(*dest, /*start_frame=*/0, /*end_frame=*/200,
                /*start_frame_offset=*/0, /*base_value=*/0.1f);
  VerifySegment(*dest, /*start_frame=*/200, /*end_frame=*/400,
                /*start_frame_offset=*/0, /*base_value=*/0.5f);
  VerifySegment(*dest, /*start_frame=*/400, /*end_frame=*/480,
                /*start_frame_offset=*/0, /*base_value=*/0.0f, /*step=*/0.0f);
}

TEST_F(ReadAloudAudioRendererTest, RenderTimeStretchingFaster) {
  media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                media::ChannelLayoutConfig::Stereo(), 48000,
                                480);
  ASSERT_TRUE(renderer_->Initialize(params, queue_.get()));

  // Set playback rate to 2.0x (double speed).
  renderer_->SetPlaybackRate(2.0);

  // We push 2880 frames of audio.
  // WSOLA needs at least 2399 frames of lookahead at 48kHz.
  // Render 1: consumes 960 frames. Remaining: 2400 (search index -239 + 2399 =
  // 2160 <= 2400). Succeeds. Render 2: consumes 960 frames. Remaining: 1920
  // (search index 241 + 2399 = 2640 <= 1920 is false). Succeeds (copies from
  // completed). Render 3: underflows (remaining < 2399, no completed frames
  // left). Returns 0.
  auto segment = GenerateSegment(params, /*frames=*/2880);
  ASSERT_TRUE(queue_->Push(segment));

  auto dest = media::AudioBus::Create(params);
  int frames_rendered = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest.get());

  EXPECT_EQ(frames_rendered, 480);

  auto dest2 = media::AudioBus::Create(params);
  int frames_rendered_2 = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest2.get());

  EXPECT_EQ(frames_rendered_2, 480);

  auto dest3 = media::AudioBus::Create(params);
  int frames_rendered_3 = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest3.get());

  EXPECT_EQ(frames_rendered_3, 0);
  EXPECT_TRUE(dest3->AreFramesZero());
}

TEST_F(ReadAloudAudioRendererTest, RenderTimeStretchingSlower) {
  media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                media::ChannelLayoutConfig::Stereo(), 48000,
                                480);
  ASSERT_TRUE(renderer_->Initialize(params, queue_.get()));

  // Set playback rate to 0.5x (half speed).
  renderer_->SetPlaybackRate(0.5);

  // We push 2400 frames of audio.
  // WSOLA needs at least 2399 frames of lookahead at 48kHz.
  // At 0.5x, search index remains negative for 5 renders, meaning no frames
  // are removed.
  // Render 1 to 5 will succeed (480 frames).
  // Render 6 will underflow.
  auto segment = GenerateSegment(params, /*frames=*/2400);
  ASSERT_TRUE(queue_->Push(segment));

  for (int i = 0; i < 5; ++i) {
    auto dest = media::AudioBus::Create(params);
    int frames_rendered = renderer_->Render(
        /*delay=*/base::TimeDelta(),
        /*delay_timestamp=*/base::TimeTicks::Now(),
        /*glitch_info=*/media::AudioGlitchInfo(), dest.get());
    EXPECT_EQ(frames_rendered, 480) << "Failed at render index " << i;
    EXPECT_FALSE(dest->AreFramesZero())
        << "Expected audio at render index " << i;
  }

  auto dest_fail = media::AudioBus::Create(params);
  int frames_rendered_fail = renderer_->Render(
      /*delay=*/base::TimeDelta(),
      /*delay_timestamp=*/base::TimeTicks::Now(),
      /*glitch_info=*/media::AudioGlitchInfo(), dest_fail.get());

  EXPECT_EQ(frames_rendered_fail, 0);
  EXPECT_TRUE(dest_fail->AreFramesZero());
}

}  // namespace readaloud
