/*
 *
 *    Copyright (c) 2023 Project CHIP 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
 *
 *        http://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.
 */

use core::time;
use std::thread;

use matter::{
    data_model::{
        core::DataModel,
        objects::{AttrValue, EncodeValue},
    },
    interaction_model::{
        core::{IMStatusCode, OpCode},
        messages::{ib::CmdData, ib::CmdPath, msg::InvReq, GenericPath},
        messages::{
            ib::{AttrData, AttrPath, AttrStatus},
            msg::{self, StatusResp, TimedReq, WriteReq, WriteResp},
        },
    },
    tlv::{self, FromTLV, TLVArray, TLVWriter, ToTLV},
    transport::exchange::{self, Exchange},
};

use crate::{
    common::{
        commands::*,
        echo_cluster,
        im_engine::{ImEngine, ImInput},
    },
    echo_req, echo_resp,
};

fn handle_timed_reqs<'a>(
    opcode: OpCode,
    request: &dyn ToTLV,
    timeout: u16,
    delay: u16,
    output: &'a mut [u8],
) -> (u8, DataModel, &'a [u8]) {
    let mut im_engine = ImEngine::new();
    // Use the same exchange for all parts of the transaction
    im_engine.exch = Some(Exchange::new(1, 0, exchange::Role::Responder));

    if timeout != 0 {
        // Send Timed Req
        let mut tmp_buf = [0u8; 400];
        let timed_req = TimedReq { timeout };
        let im_input = ImInput::new(OpCode::TimedRequest, &timed_req);
        let (_, out_buf) = im_engine.process(&im_input, &mut tmp_buf);
        tlv::print_tlv_list(out_buf);
    } else {
        println!("Skipping timed request");
    }

    // Process any delays
    let delay = time::Duration::from_millis(delay.into());
    thread::sleep(delay);

    // Send Write Req
    let input = ImInput::new(opcode, request);
    let (resp_opcode, output) = im_engine.process(&input, output);
    (resp_opcode, im_engine.dm, output)
}
enum WriteResponse<'a> {
    TransactionError,
    TransactionSuccess(&'a [AttrStatus]),
}

// Helper for handling Write Attribute sequences
fn handle_timed_write_reqs(
    input: &[AttrData],
    expected: &WriteResponse,
    timeout: u16,
    delay: u16,
) -> DataModel {
    let mut out_buf = [0u8; 400];
    let write_req = WriteReq::new(false, input);

    let (resp_opcode, dm, out_buf) = handle_timed_reqs(
        OpCode::WriteRequest,
        &write_req,
        timeout,
        delay,
        &mut out_buf,
    );
    tlv::print_tlv_list(out_buf);
    let root = tlv::get_root_node_struct(out_buf).unwrap();

    match expected {
        WriteResponse::TransactionSuccess(t) => {
            assert_eq!(
                num::FromPrimitive::from_u8(resp_opcode),
                Some(OpCode::WriteResponse)
            );
            let resp = WriteResp::from_tlv(&root).unwrap();
            assert_eq!(resp.write_responses, t);
        }
        WriteResponse::TransactionError => {
            assert_eq!(
                num::FromPrimitive::from_u8(resp_opcode),
                Some(OpCode::StatusResponse)
            );
            let status_resp = StatusResp::from_tlv(&root).unwrap();
            assert_eq!(status_resp.status, IMStatusCode::Timeout);
        }
    }
    dm
}

#[test]
fn test_timed_write_fail_and_success() {
    // - 1 Timed Attr Write Transaction should fail due to timeout
    // - 1 Timed Attr Write Transaction should succeed
    let val0 = 10;
    let _ = env_logger::try_init();
    let attr_data0 = |tag, t: &mut TLVWriter| {
        let _ = t.u16(tag, val0);
    };

    let ep_att = GenericPath::new(
        None,
        Some(echo_cluster::ID),
        Some(echo_cluster::Attributes::AttWrite as u32),
    );
    let input = &[AttrData::new(
        None,
        AttrPath::new(&ep_att),
        EncodeValue::Closure(&attr_data0),
    )];

    let ep0_att = GenericPath::new(
        Some(0),
        Some(echo_cluster::ID),
        Some(echo_cluster::Attributes::AttWrite as u32),
    );

    let ep1_att = GenericPath::new(
        Some(1),
        Some(echo_cluster::ID),
        Some(echo_cluster::Attributes::AttWrite as u32),
    );
    let expected = &[
        AttrStatus::new(&ep0_att, IMStatusCode::Sucess, 0),
        AttrStatus::new(&ep1_att, IMStatusCode::Sucess, 0),
    ];

    // Test with incorrect handling
    handle_timed_write_reqs(input, &WriteResponse::TransactionError, 400, 500);

    // Test with correct handling
    let dm = handle_timed_write_reqs(input, &WriteResponse::TransactionSuccess(expected), 400, 0);
    assert_eq!(
        AttrValue::Uint16(val0),
        dm.read_attribute_raw(
            0,
            echo_cluster::ID,
            echo_cluster::Attributes::AttWrite as u16
        )
        .unwrap()
    );
    assert_eq!(
        AttrValue::Uint16(val0),
        dm.read_attribute_raw(
            0,
            echo_cluster::ID,
            echo_cluster::Attributes::AttWrite as u16
        )
        .unwrap()
    );
}

enum TimedInvResponse<'a> {
    TransactionError(IMStatusCode),
    TransactionSuccess(&'a [ExpectedInvResp]),
}
// Helper for handling Invoke Command sequences
fn handle_timed_commands(
    input: &[CmdData],
    expected: &TimedInvResponse,
    timeout: u16,
    delay: u16,
    set_timed_request: bool,
) -> DataModel {
    let mut out_buf = [0u8; 400];
    let req = InvReq {
        suppress_response: Some(false),
        timed_request: Some(set_timed_request),
        inv_requests: Some(TLVArray::Slice(input)),
    };

    let (resp_opcode, dm, out_buf) =
        handle_timed_reqs(OpCode::InvokeRequest, &req, timeout, delay, &mut out_buf);
    tlv::print_tlv_list(out_buf);
    let root = tlv::get_root_node_struct(out_buf).unwrap();

    match expected {
        TimedInvResponse::TransactionSuccess(t) => {
            assert_eq!(
                num::FromPrimitive::from_u8(resp_opcode),
                Some(OpCode::InvokeResponse)
            );
            let resp = msg::InvResp::from_tlv(&root).unwrap();
            assert_inv_response(&resp, t)
        }
        TimedInvResponse::TransactionError(e) => {
            assert_eq!(
                num::FromPrimitive::from_u8(resp_opcode),
                Some(OpCode::StatusResponse)
            );
            let status_resp = StatusResp::from_tlv(&root).unwrap();
            assert_eq!(status_resp.status, *e);
        }
    }
    dm
}

#[test]
fn test_timed_cmd_success() {
    // A timed request that works
    let _ = env_logger::try_init();

    let input = &[echo_req!(0, 5), echo_req!(1, 10)];
    let expected = &[echo_resp!(0, 10), echo_resp!(1, 30)];
    handle_timed_commands(
        input,
        &TimedInvResponse::TransactionSuccess(expected),
        400,
        0,
        true,
    );
}

#[test]
fn test_timed_cmd_timeout() {
    // A timed request that is executed after t imeout
    let _ = env_logger::try_init();

    let input = &[echo_req!(0, 5), echo_req!(1, 10)];
    handle_timed_commands(
        input,
        &TimedInvResponse::TransactionError(IMStatusCode::Timeout),
        400,
        500,
        true,
    );
}

#[test]
fn test_timed_cmd_timedout_mismatch() {
    // A timed request with timeout mismatch
    let _ = env_logger::try_init();

    let input = &[echo_req!(0, 5), echo_req!(1, 10)];
    handle_timed_commands(
        input,
        &TimedInvResponse::TransactionError(IMStatusCode::TimedRequestMisMatch),
        400,
        0,
        false,
    );

    let input = &[echo_req!(0, 5), echo_req!(1, 10)];
    handle_timed_commands(
        input,
        &TimedInvResponse::TransactionError(IMStatusCode::TimedRequestMisMatch),
        0,
        0,
        true,
    );
}