
#include "test_common.h"
#include "./utils/matrix.h"

using utils::Vf; using utils::Vd; using utils::Vi;
using utils::Mf; using utils::Md; using utils::Mi;


// tiny helper
template <typename T>
static bool mat_is_filled(const utils::Matrix<T>& M, T v) {
    for (std::uint64_t i = 0; i < M.rows(); ++i)
        for (std::uint64_t j = 0; j < M.cols(); ++j)
            if (M(i,j) != v) return false;
    return true;
}

// ------------------- basic construction -------------------
TEST_CASE(Matrix_Default_Construct) {
    Md M;
    CHECK_EQ(M.rows(), 0, "default rows should be 0");
    CHECK_EQ(M.cols(), 0, "default cols should be 0");
}

TEST_CASE(Matrix_Filled_Construct) {
    Md M(3, 4, 2.5);
    CHECK_EQ(M.rows(), 3, "rows");
    CHECK_EQ(M.cols(), 4, "cols");
    CHECK(mat_is_filled(M, 2.5), "all elements should be 2.5");
}

// ------------------- element access / write -------------------
TEST_CASE(Matrix_Set_Get) {
    Mi M(2, 3, 0);
    M(0,0) = 42;
    M(1,2) = -7;
    CHECK_EQ(M(0,0), 42, "set/get (0,0)");
    CHECK_EQ(M(1,2), -7, "set/get (1,2)");
}

// ------------------- resize semantics -------------------
TEST_CASE(Matrix_Resize_Grow) {
    Mf M(2, 2, 1.0f);
    M.resize(3, 4, 9.0f); // grow; newly appended elements get the fill value
    CHECK_EQ(M.rows(), 3, "rows after resize");
    CHECK_EQ(M.cols(), 4, "cols after resize");
    CHECK(M(2,3) == 9.0f, "last element should be the fill value after grow");
}

TEST_CASE(Matrix_Resize_Shrink) {
    Mi M(4, 4, 5);
    M(0,0) = 11;
    M.resize(2, 2, 999); // shrink; size reduces
    CHECK_EQ(M.rows(), 2, "rows after shrink");
    CHECK_EQ(M.cols(), 2, "cols after shrink");
    // element mapping after shrink is implementation dependent; just check bounds usable
    M(1,1) = 3;
    CHECK_EQ(M(1,1), 3, "write after shrink works");
}

// ------------------- row helpers -------------------
TEST_CASE(Matrix_Get_Row) {
    Mi M(3,4,0);
    // set row 1 to [10,20,30,40]
    for (std::uint64_t j=0;j<4;++j) M(1,j) = (j+1)*10;
    auto r = M.get_row(1);
    CHECK_EQ(r.size(), 4, "row size");
    CHECK(r[0]==10 && r[1]==20 && r[2]==30 && r[3]==40, "row contents");
}

TEST_CASE(Matrix_Set_Row) {
    Mi M(2,3,0);
    utils::Vector<int64_t> v(3, 0);
    v[0]=7; v[1]=8; v[2]=9;
    M.set_row(0, v);
    CHECK(M(0,0)==7 && M(0,1)==8 && M(0,2)==9, "set_row contents");
}

TEST_CASE(Matrix_Row_OutOfRange_Throws) {
    Mi M(2,2,0);
    bool threw=false;
    try { (void)M.get_row(2); } catch(const std::out_of_range&) { threw=true; }
    CHECK(threw, "get_row out-of-range should throw");

    threw=false;
    try {
        utils::Vector<int64_t> v(3,1); // wrong size
        M.set_row(1, v);
    } catch(const std::runtime_error&) { threw=true; }
    CHECK(threw, "set_row size mismatch should throw");
}

// ------------------- col helpers -------------------
TEST_CASE(Matrix_Get_Col) {
    Mi M(3,2,0);
    M(0,1)=5; M(1,1)=6; M(2,1)=7;
    auto c = M.get_col(1);
    CHECK_EQ(c.size(), 3, "col size");
    CHECK(c[0]==5 && c[1]==6 && c[2]==7, "col contents");
}

TEST_CASE(Matrix_Set_Col) {
    Mi M(3,2,0);
    utils::Vector<int64_t> v(3, 0);
    v[0]=1; v[1]=4; v[2]=9;
    M.set_col(0, v);
    CHECK(M(0,0)==1 && M(1,0)==4 && M(2,0)==9, "set_col contents");
}

TEST_CASE(Matrix_Col_OutOfRange_Throws) {
    Mi M(2,2,0);
    bool threw=false;
    try { (void)M.get_col(2); } catch(const std::out_of_range&) { threw=true; }
    CHECK(threw, "get_col out-of-range should throw");

    threw=false;
    try {
        utils::Vector<int64_t> v(1,1); // wrong size
        M.set_col(1, v);
    } catch(const std::runtime_error&) { threw=true; }
    CHECK(threw, "set_col size mismatch should throw");
}

// ------------------- swap rows/cols -------------------
TEST_CASE(Matrix_Swap_Rows) {
    Mi M(2,3,0);
    // row0: 1 2 3, row1: 4 5 6
    for (std::uint64_t j=0;j<3;++j){ M(0,j)=j+1; M(1,j)=j+4; }
    M.swap_rows(0,1);
    CHECK(M(0,0)==4 && M(0,1)==5 && M(0,2)==6, "row0 after swap");
    CHECK(M(1,0)==1 && M(1,1)==2 && M(1,2)==3, "row1 after swap");
}

TEST_CASE(Matrix_Swap_Cols) {
    Mi M(3,3,0);
    // col0: 9 8 7, col2: 1 2 3
    M(0,0)=9; M(1,0)=8; M(2,0)=7;
    M(0,2)=1; M(1,2)=2; M(2,2)=3;
    M.swap_cols(0,2);
    CHECK(M(0,0)==1 && M(1,0)==2 && M(2,0)==3, "col0 after swap");
    CHECK(M(0,2)==9 && M(1,2)==8 && M(2,2)==7, "col2 after swap");
}

TEST_CASE(Matrix_Swap_OutOfRange_Throws) {
    Mi M(2,2,0);
    bool threw=false;
    try { M.swap_rows(0,2); } catch(const std::out_of_range&) { threw=true; }
    CHECK(threw, "swap_rows out-of-range should throw");
    threw=false;
    try { M.swap_cols(0,3); } catch(const std::out_of_range&) { threw=true; }
    CHECK(threw, "swap_cols out-of-range should throw");
}

// ------------------- stream output (basic sanity) -------------------
TEST_CASE(Matrix_Stream_ToString) {
    Mf M(2,2,0.0f);
    M(0,0)=1.0f; M(0,1)=2.0f; M(1,0)=3.0f; M(1,1)=4.0f;
    std::ostringstream os;
    os << M;
    auto s = os.str();
    CHECK(s.find("[[") != std::string::npos, "starts with [[");
    CHECK(s.find("1.000") != std::string::npos, "contains formatted 1.000");
    CHECK(s.find("4.000") != std::string::npos, "contains formatted 4.000");
}