/*
 * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */
#include <unity.h>
#include <esp_matter.h>
#include <esp_matter_core.h>
#include <nvs_flash.h>

using namespace esp_matter;
using namespace chip::app::Clusters;

static node_t *test_node = nullptr;
static endpoint_t *test_endpoint = nullptr;
static uint16_t test_endpoint_id = 0;

struct callback_record_t {
    bool called;
    attribute::callback_type_t type;
    uint16_t endpoint_id;
    uint32_t cluster_id;
    uint32_t attribute_id;
    esp_matter_attr_val_t val;
};

static callback_record_t cb_pre_update;
static callback_record_t cb_post_update;

static void reset_callback_records()
{
    memset(&cb_pre_update, 0, sizeof(cb_pre_update));
    memset(&cb_post_update, 0, sizeof(cb_post_update));
}

static esp_err_t test_attribute_callback(attribute::callback_type_t type, uint16_t endpoint_id,
                                         uint32_t cluster_id, uint32_t attribute_id,
                                         esp_matter_attr_val_t *val, void *priv_data)
{
    callback_record_t *record = nullptr;
    if (type == attribute::PRE_UPDATE) {
        record = &cb_pre_update;
    } else if (type == attribute::POST_UPDATE) {
        record = &cb_post_update;
    } else {
        return ESP_OK;
    }
    record->called = true;
    record->type = type;
    record->endpoint_id = endpoint_id;
    record->cluster_id = cluster_id;
    record->attribute_id = attribute_id;
    record->val = *val;
    return ESP_OK;
}

void setup_for_update_report()
{
    static bool setup_done = false;
    if (setup_done) {
        return;
    }

    esp_err_t err = nvs_flash_init();
    TEST_ASSERT_EQUAL(ESP_OK, err);

    node::config_t node_config;
    test_node = node::create(&node_config, test_attribute_callback, nullptr);
    TEST_ASSERT_NOT_NULL(test_node);

    endpoint::extended_color_light::config_t light_config;
    light_config.on_off.on_off = false;
    light_config.level_control.current_level = nullable<uint8_t>(100);
    test_endpoint = endpoint::extended_color_light::create(test_node, &light_config, ENDPOINT_FLAG_NONE, nullptr);
    TEST_ASSERT_NOT_NULL(test_endpoint);

    test_endpoint_id = endpoint::get_id(test_endpoint);

    err = esp_matter::start(nullptr);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    setup_done = true;
}

void teardown_for_update_report()
{
    reset_callback_records();
}

// ============================================================
// attribute::report() tests
// ============================================================

TEST_CASE("report returns ESP_ERR_INVALID_ARG for null val", "[report][invalid]")
{
    setup_for_update_report();

    esp_err_t err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, nullptr);
    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err);

    teardown_for_update_report();
}

TEST_CASE("report returns ESP_ERR_INVALID_ARG for invalid endpoint", "[report][invalid]")
{
    setup_for_update_report();

    esp_matter_attr_val_t val = esp_matter_bool(true);
    esp_err_t err = attribute::report(chip::kInvalidEndpointId, OnOff::Id, OnOff::Attributes::OnOff::Id, &val);
    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err);

    teardown_for_update_report();
}

TEST_CASE("report returns ESP_ERR_INVALID_ARG for invalid cluster", "[report][invalid]")
{
    setup_for_update_report();

    esp_matter_attr_val_t val = esp_matter_bool(true);
    esp_err_t err = attribute::report(test_endpoint_id, chip::kInvalidClusterId, OnOff::Attributes::OnOff::Id, &val);
    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err);

    teardown_for_update_report();
}

TEST_CASE("report returns ESP_ERR_INVALID_ARG for invalid attribute", "[report][invalid]")
{
    setup_for_update_report();

    esp_matter_attr_val_t val = esp_matter_bool(true);
    esp_err_t err = attribute::report(test_endpoint_id, OnOff::Id, chip::kInvalidAttributeId, &val);
    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err);

    teardown_for_update_report();
}

TEST_CASE("report bool updates stored value", "[report][bool]")
{
    setup_for_update_report();

    esp_matter_attr_val_t val = esp_matter_bool(true);
    esp_err_t err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    esp_matter_attr_val_t retrieved;
    err = attribute::get_val(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &retrieved);
    TEST_ASSERT_EQUAL(ESP_OK, err);
    TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, retrieved.type);
    TEST_ASSERT_EQUAL(true, retrieved.val.b);

    val = esp_matter_bool(false);
    err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    err = attribute::get_val(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &retrieved);
    TEST_ASSERT_EQUAL(ESP_OK, err);
    TEST_ASSERT_EQUAL(false, retrieved.val.b);

    teardown_for_update_report();
}

TEST_CASE("report nullable uint8 updates stored value", "[report][nullable][uint8]")
{
    setup_for_update_report();

    esp_matter_attr_val_t val = esp_matter_nullable_uint8(150);
    esp_err_t err = attribute::report(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    esp_matter_attr_val_t retrieved;
    err = attribute::get_val(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &retrieved);
    TEST_ASSERT_EQUAL(ESP_OK, err);
    TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT8, retrieved.type);
    nullable<uint8_t> data(retrieved.val.u8);
    TEST_ASSERT_FALSE(data.is_null());
    TEST_ASSERT_EQUAL(150, data.value());

    teardown_for_update_report();
}

TEST_CASE("report same value returns ESP_OK", "[report][no_change]")
{
    setup_for_update_report();

    esp_matter_attr_val_t val = esp_matter_bool(false);
    esp_err_t err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    teardown_for_update_report();
}

// ============================================================
// report() must NOT invoke attribute callbacks
// ============================================================

TEST_CASE("report does not call attribute callbacks", "[report][callback]")
{
    setup_for_update_report();
    reset_callback_records();

    esp_matter_attr_val_t val = esp_matter_nullable_uint8(42);
    esp_err_t err = attribute::report(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    TEST_ASSERT_FALSE(cb_pre_update.called);
    TEST_ASSERT_FALSE(cb_post_update.called);

    teardown_for_update_report();
}

// ============================================================
// update() MUST invoke attribute callbacks with correct values
// ============================================================

TEST_CASE("update calls PRE_UPDATE and POST_UPDATE callbacks", "[update][callback]")
{
    setup_for_update_report();
    reset_callback_records();

    esp_matter_attr_val_t val = esp_matter_bool(true);
    esp_err_t err = attribute::update(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    TEST_ASSERT_TRUE(cb_pre_update.called);
    TEST_ASSERT_EQUAL(attribute::PRE_UPDATE, cb_pre_update.type);
    TEST_ASSERT_EQUAL(test_endpoint_id, cb_pre_update.endpoint_id);
    TEST_ASSERT_EQUAL(OnOff::Id, cb_pre_update.cluster_id);
    TEST_ASSERT_EQUAL(OnOff::Attributes::OnOff::Id, cb_pre_update.attribute_id);
    TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, cb_pre_update.val.type);
    TEST_ASSERT_EQUAL(true, cb_pre_update.val.val.b);

    TEST_ASSERT_TRUE(cb_post_update.called);
    TEST_ASSERT_EQUAL(attribute::POST_UPDATE, cb_post_update.type);
    TEST_ASSERT_EQUAL(test_endpoint_id, cb_post_update.endpoint_id);
    TEST_ASSERT_EQUAL(OnOff::Id, cb_post_update.cluster_id);
    TEST_ASSERT_EQUAL(OnOff::Attributes::OnOff::Id, cb_post_update.attribute_id);
    TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, cb_post_update.val.type);
    TEST_ASSERT_EQUAL(true, cb_post_update.val.val.b);

    teardown_for_update_report();
}

TEST_CASE("update calls callbacks with correct value for nullable uint8", "[update][callback][nullable][uint8]")
{
    setup_for_update_report();
    reset_callback_records();

    esp_matter_attr_val_t val = esp_matter_nullable_uint8(77);
    esp_err_t err = attribute::update(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    TEST_ASSERT_TRUE(cb_pre_update.called);
    TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT8, cb_pre_update.val.type);
    TEST_ASSERT_EQUAL(77, cb_pre_update.val.val.u8);

    TEST_ASSERT_TRUE(cb_post_update.called);
    TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT8, cb_post_update.val.type);
    TEST_ASSERT_EQUAL(77, cb_post_update.val.val.u8);

    teardown_for_update_report();
}

// ============================================================
// update() and report() behavioral parity (stored value)
// ============================================================

TEST_CASE("update and report both store the value correctly", "[update][report_parity]")
{
    setup_for_update_report();

    esp_matter_attr_val_t val = esp_matter_nullable_uint8(55);
    esp_err_t err = attribute::update(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    esp_matter_attr_val_t after_update;
    err = attribute::get_val(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &after_update);
    TEST_ASSERT_EQUAL(ESP_OK, err);
    TEST_ASSERT_EQUAL(55, after_update.val.u8);

    val = esp_matter_nullable_uint8(88);
    err = attribute::report(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val);
    TEST_ASSERT_EQUAL(ESP_OK, err);

    esp_matter_attr_val_t after_report;
    err = attribute::get_val(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &after_report);
    TEST_ASSERT_EQUAL(ESP_OK, err);
    TEST_ASSERT_EQUAL(88, after_report.val.u8);

    TEST_ASSERT_EQUAL(after_update.type, after_report.type);

    teardown_for_update_report();
}
