// Copyright 2024 The Pigweed Authors
//
// Licensed 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
//
//     https://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.
#include "pw_digital_io/digital_io.h"

#include <linux/gpio.h>

#include <algorithm>
#include <functional>
#include <memory>
#include <mutex>
#include <queue>
#include <vector>

#include "mock_vfs.h"
#include "pw_digital_io_linux/digital_io.h"
#include "pw_log/log.h"
#include "pw_result/result.h"
#include "pw_sync/mutex.h"
#include "pw_sync/timed_thread_notification.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
#include "pw_unit_test/framework.h"
#include "test_utils.h"

namespace pw::digital_io {
namespace {

using namespace std::chrono_literals;

class DigitalIoTest;
class LineHandleFile;
class LineEventFile;

// Represents a mocked in-kernel GPIO line object.
class Line {
 public:
  //
  // Harness-side interface: Intended for use by DigitalIoTest and the
  // MockFile subclasses.
  //

  explicit Line(uint32_t index) : index_(index) {}

  // Get the logical value of the line, respecting active_low.
  Result<bool> GetValue() const {
    // Linux lets you read the value of an output.
    if (requested_ == RequestedState::kNone) {
      PW_LOG_ERROR("Cannot get value of unrequested line");
      return Status::FailedPrecondition();
    }
    return physical_state_ ^ active_low_;
  }

  // Set the logical value of the line, respecting active_low.
  // Returns OK on success; FAILED_PRECONDITION if not requested as output.
  Status SetValue(bool value) {
    if (requested_ != RequestedState::kOutput) {
      PW_LOG_ERROR("Cannot set value of line not requested as output");
      return Status::FailedPrecondition();
    }

    physical_state_ = value ^ active_low_;

    PW_LOG_DEBUG("Set line %u to physical %u", index_, physical_state_);
    return OkStatus();
  }

  Status RequestInput(LineHandleFile* handle, bool active_low) {
    PW_TRY(DoRequest(RequestedState::kInput, active_low));
    current_line_handle_ = handle;
    return OkStatus();
  }

  Status RequestInputInterrupt(LineEventFile* handle, bool active_low) {
    PW_TRY(DoRequest(RequestedState::kInputInterrupt, active_low));
    current_event_handle_ = handle;
    return OkStatus();
  }

  Status RequestOutput(LineHandleFile* handle, bool active_low) {
    PW_TRY(DoRequest(RequestedState::kOutput, active_low));
    current_line_handle_ = handle;
    return OkStatus();
  }

  void ClearRequest() {
    requested_ = RequestedState::kNone;
    current_line_handle_ = nullptr;
    current_event_handle_ = nullptr;
  }

  //
  // Test-side interface: Intended for use by the tests themselves.
  //

  enum class RequestedState {
    kNone,            // Not requested by "userspace"
    kInput,           // Requested by "userspace" as an input
    kInputInterrupt,  // Requested by "userspace" as an interrupt (event)
    kOutput,          // Requested by "userspace" as an output
  };

  RequestedState requested() const { return requested_; }
  LineHandleFile* current_line_handle() const { return current_line_handle_; }
  LineEventFile* current_event_handle() const { return current_event_handle_; }

  void ForcePhysicalState(bool state) { physical_state_ = state; }

  bool physical_state() const { return physical_state_; }

 private:
  const uint32_t index_;
  bool physical_state_ = false;

  RequestedState requested_ = RequestedState::kNone;
  bool active_low_ = false;

  LineHandleFile* current_line_handle_ = nullptr;
  LineEventFile* current_event_handle_ = nullptr;

  Status DoRequest(RequestedState request, bool active_low) {
    if (requested_ != RequestedState::kNone) {
      PW_LOG_ERROR("Cannot request already-requested line");
      return Status::FailedPrecondition();
    }
    requested_ = request;
    active_low_ = active_low;
    return OkStatus();
  }
};

#define EXPECT_LINE_NOT_REQUESTED(line) \
  EXPECT_EQ(line.requested(), Line::RequestedState::kNone)
#define EXPECT_LINE_REQUESTED_OUTPUT(line) \
  EXPECT_EQ(line.requested(), Line::RequestedState::kOutput)
#define EXPECT_LINE_REQUESTED_INPUT(line) \
  EXPECT_EQ(line.requested(), Line::RequestedState::kInput)
#define EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line) \
  EXPECT_EQ(line.requested(), Line::RequestedState::kInputInterrupt)

// Represents a GPIO line handle, the result of issuing
// GPIO_GET_LINEHANDLE_IOCTL to an open chip file.
class LineHandleFile : public MockFile {
 public:
  LineHandleFile(MockVfs& vfs, int eventfd, const std::string& name, Line& line)
      : MockFile(vfs, eventfd, name), line_(line) {}

 private:
  Line& line_;

  //
  // MockFile impl.
  //

  int DoClose() override {
    line_.ClearRequest();
    return 0;
  }

  int DoIoctl(unsigned long request, void* arg) override {
    switch (request) {
      case GPIOHANDLE_GET_LINE_VALUES_IOCTL:
        return DoIoctlGetValues(static_cast<struct gpiohandle_data*>(arg));
      case GPIOHANDLE_SET_LINE_VALUES_IOCTL:
        return DoIoctlSetValues(static_cast<struct gpiohandle_data*>(arg));
      default:
        PW_LOG_ERROR("%s: Unhandled request=0x%lX", __FUNCTION__, request);
        return -1;
    }
  }

  // Handle GPIOHANDLE_GET_LINE_VALUES_IOCTL
  int DoIoctlGetValues(struct gpiohandle_data* data) {
    auto result = line_.GetValue();
    if (!result.ok()) {
      return -1;
    }

    data->values[0] = *result;
    return 0;
  }

  // Handle GPIOHANDLE_SET_LINE_VALUES_IOCTL
  int DoIoctlSetValues(struct gpiohandle_data* data) {
    auto status = line_.SetValue(data->values[0]);
    if (!status.ok()) {
      return -1;
    }

    return 0;
  }
};

// Represents a GPIO line event handle, the result of issuing
// GPIO_GET_LINEEVENT_IOCTL to an open chip file.
class LineEventFile final : public MockFile {
 public:
  LineEventFile(MockVfs& vfs,
                int eventfd,
                const std::string& name,
                Line& line,
                uint32_t event_flags)
      : MockFile(vfs, eventfd, name), line_(line), event_flags_(event_flags) {}

  void EnqueueEvent(const struct gpioevent_data& event) {
    static_assert(GPIOEVENT_REQUEST_RISING_EDGE == GPIOEVENT_EVENT_RISING_EDGE);
    static_assert(GPIOEVENT_REQUEST_FALLING_EDGE ==
                  GPIOEVENT_EVENT_FALLING_EDGE);
    if ((event.id & event_flags_) == 0) {
      return;
    }

    {
      std::lock_guard lock(mutex_);
      event_queue_.push(event);
    }

    // Make this file's fd readable (one token).
    WriteEventfd();
  }

 private:
  Line& line_;
  uint32_t const event_flags_;
  std::queue<struct gpioevent_data> event_queue_;
  pw::sync::Mutex mutex_;

  // Hide these
  using MockFile::ReadEventfd;
  using MockFile::WriteEventfd;

  //
  // MockFile impl.
  //

  int DoClose() override {
    line_.ClearRequest();
    return 0;
  }

  int DoIoctl(unsigned long request, void* arg) override {
    switch (request) {
      case GPIOHANDLE_GET_LINE_VALUES_IOCTL:
        return DoIoctlGetValues(static_cast<struct gpiohandle_data*>(arg));
      // Unlinke LineHandleFile, this only supports "get", as it is only for
      // inputs.
      default:
        PW_LOG_ERROR("%s: Unhandled request=0x%lX", __FUNCTION__, request);
        return -1;
    }
  }

  int DoIoctlGetValues(struct gpiohandle_data* data) {
    auto result = line_.GetValue();
    if (!result.ok()) {
      return -1;
    }

    data->values[0] = *result;
    return 0;
  }

  ssize_t DoRead(void* buf, size_t count) override {
    // Consume the readable state of the eventfd (one token).
    PW_CHECK_INT_EQ(ReadEventfd(), 1);  // EFD_SEMAPHORE

    std::lock_guard lock(mutex_);

    // Pop the event from the queue.
    PW_CHECK(!event_queue_.empty());
    struct gpioevent_data event = event_queue_.front();
    if (count < sizeof(event)) {
      return -1;
    }
    event_queue_.pop();

    memcpy(buf, &event, sizeof(event));
    return sizeof(event);
  }
};

// Represents an open GPIO chip file, the result of opening /dev/gpiochip*.
class ChipFile : public MockFile {
 public:
  ChipFile(MockVfs& vfs,
           int eventfd,
           const std::string& name,
           std::vector<Line>& lines)
      : MockFile(vfs, eventfd, name), lines_(lines) {}

 private:
  std::vector<Line>& lines_;

  //
  // MockFile impl.
  //

  int DoIoctl(unsigned long request, void* arg) override {
    switch (request) {
      case GPIO_GET_LINEHANDLE_IOCTL:
        return DoLinehandleIoctl(static_cast<struct gpiohandle_request*>(arg));
      case GPIO_GET_LINEEVENT_IOCTL:
        return DoLineeventIoctl(static_cast<struct gpioevent_request*>(arg));
      default:
        PW_LOG_ERROR("%s: Unhandled request=0x%lX", __FUNCTION__, request);
        return -1;
    }
  }

  // Handle GPIO_GET_LINEHANDLE_IOCTL
  int DoLinehandleIoctl(struct gpiohandle_request* req) {
    uint32_t const direction =
        req->flags & (GPIOHANDLE_REQUEST_OUTPUT | GPIOHANDLE_REQUEST_INPUT);

    // Validate flags.
    if (direction == (GPIOHANDLE_REQUEST_OUTPUT | GPIOHANDLE_REQUEST_INPUT)) {
      PW_LOG_ERROR("%s: OUTPUT and INPUT are mutually exclusive", __FUNCTION__);
      return -1;
    }

    // Only support requesting one line at at time.
    if (req->lines != 1) {
      PW_LOG_ERROR("%s: Unsupported req->lines=%u", __FUNCTION__, req->lines);
      return -1;
    }

    uint32_t const offset = req->lineoffsets[0];
    uint8_t const default_value = req->default_values[0];
    bool const active_low = req->flags & GPIOHANDLE_REQUEST_ACTIVE_LOW;

    if (offset >= lines_.size()) {
      PW_LOG_ERROR("%s: Invalid line offset: %u", __FUNCTION__, offset);
      return -1;
    }
    Line& line = lines_[offset];

    auto file = vfs().MakeFile<LineHandleFile>("line-handle", line);
    // Ownership: The vfs owns this file, but the line borrows a reference to
    // it. This is safe because the file's Close() method undoes that borrow.

    Status status = OkStatus();
    switch (direction) {
      case GPIOHANDLE_REQUEST_OUTPUT:
        status.Update(line.RequestOutput(file.get(), active_low));
        status.Update(line.SetValue(default_value));
        break;
      case GPIOHANDLE_REQUEST_INPUT:
        status.Update(line.RequestInput(file.get(), active_low));
        break;
    }
    if (!status.ok()) {
      return -1;
    }

    req->fd = vfs().InstallFile(std::move(file));
    return 0;
  }

  int DoLineeventIoctl(struct gpioevent_request* req) {
    uint32_t const direction = req->handleflags & (GPIOHANDLE_REQUEST_OUTPUT |
                                                   GPIOHANDLE_REQUEST_INPUT);
    bool const active_low = req->handleflags & GPIOHANDLE_REQUEST_ACTIVE_LOW;
    uint32_t const offset = req->lineoffset;

    if (direction != GPIOHANDLE_REQUEST_INPUT) {
      PW_LOG_ERROR("%s: Only input is supported by this ioctl", __FUNCTION__);
      return -1;
    }

    if (offset >= lines_.size()) {
      PW_LOG_ERROR("%s: Invalid line offset: %u", __FUNCTION__, offset);
      return -1;
    }
    Line& line = lines_[offset];

    auto file =
        vfs().MakeFile<LineEventFile>("line-event", line, req->eventflags);
    // Ownership: The vfs() owns this file, but the line borrows a reference to
    // it. This is safe because the file's Close() method undoes that borrow.

    Status status = line.RequestInputInterrupt(file.get(), active_low);
    if (!status.ok()) {
      return -1;
    }

    req->fd = vfs().InstallFile(std::move(file));
    return 0;
  }
};

// Test fixture for all digtal io tests.
class DigitalIoTest : public ::testing::Test {
 protected:
  void SetUp() override { GetMockVfs().Reset(); }

  void TearDown() override { EXPECT_TRUE(GetMockVfs().AllFdsClosed()); }

  LinuxDigitalIoChip OpenChip() {
    int fd = GetMockVfs().InstallNewFile<ChipFile>("chip", lines_);
    return LinuxDigitalIoChip(fd);
  }

  Line& line0() { return lines_[0]; }
  Line& line1() { return lines_[1]; }

 private:
  std::vector<Line> lines_ = std::vector<Line>{
      Line(0),  // Input
      Line(1),  // Output
  };
};

//
// Tests
//

TEST_F(DigitalIoTest, DoInput) {
  LinuxDigitalIoChip chip = OpenChip();

  auto& line = line0();
  LinuxInputConfig config(
      /* index= */ 0,
      /* polarity= */ Polarity::kActiveHigh);

  ASSERT_OK_AND_ASSIGN(auto input, chip.GetInputLine(config));

  // Enable the input, and ensure it is requested.
  EXPECT_LINE_NOT_REQUESTED(line);
  ASSERT_OK(input.Enable());
  EXPECT_LINE_REQUESTED_INPUT(line);

  Result<State> state;

  // Force the line high and assert it is seen as active (active high).
  line.ForcePhysicalState(true);
  state = input.GetState();
  ASSERT_OK(state.status());
  ASSERT_EQ(State::kActive, state.value());

  // Force the line low and assert it is seen as inactive (active high).
  line.ForcePhysicalState(false);
  state = input.GetState();
  ASSERT_OK(state.status());
  ASSERT_EQ(State::kInactive, state.value());

  // Disable the line and ensure it is no longer requested.
  ASSERT_OK(input.Disable());
  EXPECT_LINE_NOT_REQUESTED(line);
}

TEST_F(DigitalIoTest, DoInputInvert) {
  LinuxDigitalIoChip chip = OpenChip();

  auto& line = line0();
  LinuxInputConfig config(
      /* index= */ 0,
      /* polarity= */ Polarity::kActiveLow);

  ASSERT_OK_AND_ASSIGN(auto input, chip.GetInputLine(config));

  // Enable the input, and ensure it is requested.
  EXPECT_LINE_NOT_REQUESTED(line);
  ASSERT_OK(input.Enable());
  EXPECT_LINE_REQUESTED_INPUT(line);

  Result<State> state;

  // Force the line high and assert it is seen as inactive (active low).
  line.ForcePhysicalState(true);
  state = input.GetState();
  ASSERT_OK(state.status());
  ASSERT_EQ(State::kInactive, state.value());

  // Force the line low and assert it is seen as active (active low).
  line.ForcePhysicalState(false);
  state = input.GetState();
  ASSERT_OK(state.status());
  ASSERT_EQ(State::kActive, state.value());

  // Disable the line and ensure it is no longer requested.
  ASSERT_OK(input.Disable());
  EXPECT_LINE_NOT_REQUESTED(line);
}

TEST_F(DigitalIoTest, DoOutput) {
  LinuxDigitalIoChip chip = OpenChip();

  auto& line = line1();
  LinuxOutputConfig config(
      /* index= */ 1,
      /* polarity= */ Polarity::kActiveHigh,
      /* default_state= */ State::kActive);

  ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config));

  // Enable the output, and ensure it is requested.
  EXPECT_LINE_NOT_REQUESTED(line);
  ASSERT_OK(output.Enable());
  EXPECT_LINE_REQUESTED_OUTPUT(line);

  // Expect the line to go high, due to default_state=kActive (active high).
  ASSERT_TRUE(line.physical_state());

  // Set the output's state to inactive, and assert it goes low (active high).
  ASSERT_OK(output.SetStateInactive());
  ASSERT_FALSE(line.physical_state());

  // Set the output's state to active, and assert it goes high (active high).
  ASSERT_OK(output.SetStateActive());
  ASSERT_TRUE(line.physical_state());

  // Disable the line and ensure it is no longer requested.
  ASSERT_OK(output.Disable());
  EXPECT_LINE_NOT_REQUESTED(line);
  // NOTE: We do not assert line.physical_state() here.
  // See the warning on LinuxDigitalOut in docs.rst.
}

TEST_F(DigitalIoTest, DoOutputInvert) {
  LinuxDigitalIoChip chip = OpenChip();

  auto& line = line1();
  LinuxOutputConfig config(
      /* index= */ 1,
      /* polarity= */ Polarity::kActiveLow,
      /* default_state= */ State::kActive);

  ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config));

  // Enable the output, and ensure it is requested.
  EXPECT_LINE_NOT_REQUESTED(line);
  ASSERT_OK(output.Enable());
  EXPECT_LINE_REQUESTED_OUTPUT(line);

  // Expect the line to stay low, due to default_state=kActive (active low).
  ASSERT_FALSE(line.physical_state());

  // Set the output's state to inactive, and assert it goes high (active low).
  ASSERT_OK(output.SetStateInactive());
  ASSERT_TRUE(line.physical_state());

  // Set the output's state to active, and assert it goes low (active low).
  ASSERT_OK(output.SetStateActive());
  ASSERT_FALSE(line.physical_state());

  // Disable the line and ensure it is no longer requested.
  ASSERT_OK(output.Disable());
  EXPECT_LINE_NOT_REQUESTED(line);
  // NOTE: We do not assert line.physical_state() here.
  // See the warning on LinuxDigitalOut in docs.rst.
}

// Verify we can get the state of an output.
TEST_F(DigitalIoTest, OutputGetState) {
  LinuxDigitalIoChip chip = OpenChip();

  auto& line = line1();
  LinuxOutputConfig config(
      /* index= */ 1,
      /* polarity= */ Polarity::kActiveHigh,
      /* default_state= */ State::kInactive);

  ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config));

  ASSERT_OK(output.Enable());

  // Expect the line to stay low, due to default_state=kInactive (active high).
  ASSERT_FALSE(line.physical_state());

  Result<State> state;

  // Verify GetState() returns the expected state: inactive (default_state).
  state = output.GetState();
  ASSERT_OK(state.status());
  ASSERT_EQ(State::kInactive, state.value());

  // Set the output's state to active, then verify GetState() returns the
  // new expected state.
  ASSERT_OK(output.SetStateActive());
  state = output.GetState();
  ASSERT_OK(state.status());
  ASSERT_EQ(State::kActive, state.value());
}

//
// Input interrupts
//

TEST_F(DigitalIoTest, DoInputInterruptsEnabledBefore) {
  LinuxDigitalIoChip chip = OpenChip();
  ASSERT_OK_AND_ASSIGN(auto notifier, LinuxGpioNotifier::Create());

  auto& line = line0();
  LinuxInputConfig config(
      /* index= */ 0,
      /* polarity= */ Polarity::kActiveHigh);

  ASSERT_OK_AND_ASSIGN(auto input, chip.GetInterruptLine(config, notifier));

  EXPECT_LINE_NOT_REQUESTED(line);

  // Have to set a handler before we can enable interrupts.
  ASSERT_OK(input.SetInterruptHandler(InterruptTrigger::kActivatingEdge,
                                      [](State) {}));

  // pw_digital_io says the line should be enabled before calling
  // EnableInterruptHandler(), but we explicitly support it being called with
  // the line disabled to avoid an unnecessary file close/reopen.
  ASSERT_OK(input.EnableInterruptHandler());
  ASSERT_OK(input.Enable());

  // Interrupts requested; should be a line event handle.
  EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line);

  // Disable; nothing should be requested.
  ASSERT_OK(input.Disable());
  EXPECT_LINE_NOT_REQUESTED(line);
}

TEST_F(DigitalIoTest, DoInputInterruptsEnabledAfter) {
  LinuxDigitalIoChip chip = OpenChip();
  ASSERT_OK_AND_ASSIGN(auto notifier, LinuxGpioNotifier::Create());

  auto& line = line0();
  LinuxInputConfig config(
      /* index= */ 0,
      /* polarity= */ Polarity::kActiveHigh);

  ASSERT_OK_AND_ASSIGN(auto input, chip.GetInterruptLine(config, notifier));

  EXPECT_LINE_NOT_REQUESTED(line);

  ASSERT_OK(input.Enable());

  // No interrupts requested; should be a normal line handle.
  EXPECT_LINE_REQUESTED_INPUT(line);

  // Interrupts requested while enabled; should be a line event handle.
  // Have to set a handler before we can enable interrupts.
  ASSERT_OK(input.SetInterruptHandler(InterruptTrigger::kActivatingEdge,
                                      [](State) {}));
  ASSERT_OK(input.EnableInterruptHandler());
  EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line);

  // Interrupts disabled while enabled; should revert to a normal line handle.
  ASSERT_OK(input.DisableInterruptHandler());
  EXPECT_LINE_REQUESTED_INPUT(line);

  // Disable; nothing should be requested.
  ASSERT_OK(input.Disable());
  EXPECT_LINE_NOT_REQUESTED(line);
}

TEST_F(DigitalIoTest, DoInputInterruptsReadOne) {
  LinuxDigitalIoChip chip = OpenChip();
  ASSERT_OK_AND_ASSIGN(auto notifier, LinuxGpioNotifier::Create());

  auto& line = line0();
  LinuxInputConfig config(
      /* index= */ 0,
      /* polarity= */ Polarity::kActiveHigh);

  ASSERT_OK_AND_ASSIGN(auto input, chip.GetInterruptLine(config, notifier));

  std::vector<State> interrupts;
  auto handler = [&interrupts](State state) {
    PW_LOG_DEBUG("Interrupt handler fired with state=%s",
                 state == State::kActive ? "active" : "inactive");
    interrupts.push_back(state);
  };

  ASSERT_OK(
      input.SetInterruptHandler(InterruptTrigger::kActivatingEdge, handler));

  // pw_digital_io says the line should be enabled before calling
  // EnableInterruptHandler(), but we explicitly support it being called with
  // the line disabled to avoid an unnecessary file close/reopen.
  ASSERT_OK(input.EnableInterruptHandler());
  ASSERT_OK(input.Enable());

  EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line);
  LineEventFile* evt = line.current_event_handle();
  ASSERT_NE(evt, nullptr);

  evt->EnqueueEvent({
      .timestamp = 1122334455667788,
      .id = GPIOEVENT_EVENT_RISING_EDGE,
  });

  constexpr int timeout = 0;  // Don't block
  PW_LOG_DEBUG("WaitForEvents(%d)", timeout);
  ASSERT_OK_AND_ASSIGN(unsigned int count, notifier->WaitForEvents(timeout));
  EXPECT_EQ(count, 1u);

  EXPECT_EQ(interrupts,
            std::vector<State>({
                State::kActive,
            }));
}

TEST_F(DigitalIoTest, DoInputInterruptsThread) {
  LinuxDigitalIoChip chip = OpenChip();
  ASSERT_OK_AND_ASSIGN(auto notifier, LinuxGpioNotifier::Create());

  auto& line = line0();
  LinuxInputConfig config(
      /* index= */ 0,
      /* polarity= */ Polarity::kActiveHigh);

  ASSERT_OK_AND_ASSIGN(auto input, chip.GetInterruptLine(config, notifier));

  constexpr unsigned int kCount = 10;
  struct {
    sync::TimedThreadNotification done;
    std::vector<State> interrupts;

    void HandleInterrupt(State state) {
      interrupts.push_back(state);
      if (interrupts.size() == kCount) {
        done.release();
      }
    }
  } context;

  auto handler = [&context](State state) {
    PW_LOG_DEBUG("Interrupt handler fired with state=%s",
                 state == State::kActive ? "active" : "inactive");
    context.HandleInterrupt(state);
  };

  ASSERT_OK(input.SetInterruptHandler(InterruptTrigger::kBothEdges, handler));

  // pw_digital_io says the line should be enabled before calling
  // EnableInterruptHandler(), but we explicitly support it being called with
  // the line disabled to avoid an unnecessary file close/reopen.
  ASSERT_OK(input.EnableInterruptHandler());
  ASSERT_OK(input.Enable());

  // Run a notifier thread.
  pw::thread::Thread notif_thread(pw::thread::stl::Options(), *notifier);

  EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line);
  LineEventFile* evt = line.current_event_handle();
  ASSERT_NE(evt, nullptr);

  // Feed the line with events.
  auto nth_event = [](unsigned int i) -> uint32_t {
    return (i % 2) ? GPIOEVENT_EVENT_FALLING_EDGE : GPIOEVENT_EVENT_RISING_EDGE;
  };
  auto nth_state = [](unsigned int i) -> State {
    return (i % 2) ? State::kInactive : State::kActive;
  };

  for (unsigned int i = 0; i < kCount; i++) {
    evt->EnqueueEvent({
        .timestamp = 1122334400000000u + i,
        .id = nth_event(i),
    });
  }

  // Wait for the notifier to pick them all up.
  constexpr auto kWaitForDataTimeout = 1000ms;
  ASSERT_TRUE(context.done.try_acquire_for(kWaitForDataTimeout));

  // Stop the notifier thread.
  notifier->CancelWait();
  notif_thread.join();

  // Verify we received all of the expected callbacks.
  EXPECT_EQ(context.interrupts.size(), kCount);
  for (unsigned int i = 0; i < kCount; i++) {
    EXPECT_EQ(context.interrupts[i], nth_state(i));
  }
}

}  // namespace
}  // namespace pw::digital_io
