<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Lapor;
use App\Services\DecisionService;
use Illuminate\Foundation\Testing\RefreshDatabase;

/**
 * ============================================================
 * WHITE BOX TESTING — DecisionService
 * ============================================================
 * Metode  : White Box Testing (Glass Box / Structural Testing)
 * Teknik  : Branch Coverage, Path Coverage, Condition Coverage
 * Target  : DecisionService::hitungAHP()
 *           DecisionService::hitungTOPSIS()
 *           DecisionService::getDetailTOPSIS()
 * Tools   : PHPUnit + Laravel TestCase + RefreshDatabase
 * ============================================================
 */
class DecisionServiceTest extends TestCase
{
    use RefreshDatabase;

    protected DecisionService $decision;
    protected $user;

    // ============================================================
    // SETUP
    // ============================================================

    protected function setUp(): void
    {
        parent::setUp();

        $this->decision = new DecisionService();
        $this->user     = User::factory()->create();
    }

    // ============================================================
    // HELPER — buat satu laporan
    // ============================================================

    private function buatLapor(array $override = []): Lapor
    {
        $default = [
            'user_id'          => $this->user->id,
            'nama_petani'      => 'Petani Test',
            'lokasi'           => 'Gowa',
            'koordinat'        => '-5.1,119.4',
            'lat'              => '-5.1',
            'lng'              => '119.4',
            'jenis_hama'       => 'Wereng',
            'tingkat_serangan' => 'Berat',
            'luas_lahan'       => 'luas_besar',
            'lama_kejadian'    => 'lama',
            'deskripsi'        => 'Testing',
            'status'           => 'menunggu',
            'foto'             => 'test.jpg',
        ];

        return Lapor::create(array_merge($default, $override));
    }

    // ============================================================
    // KELOMPOK 1 — AHP (hitungAHP)
    // ============================================================

    /**
     * [AHP-01]
     * Path  : Eksekusi penuh hitungAHP()
     * Branch: Semua loop berjalan normal (n=4)
     * Ekspektasi: Return array dengan key 'bobot' dan 'CR'
     */
    public function test_AHP01_return_key_bobot_dan_CR()
    {
        $result = $this->decision->hitungAHP();

        $this->assertIsArray($result);
        $this->assertArrayHasKey('bobot', $result);
        $this->assertArrayHasKey('CR', $result);
    }

    /**
     * [AHP-02]
     * Path  : Verifikasi jumlah elemen bobot = n (4 kriteria)
     * Branch: Loop $i dari 0..3 menghasilkan 4 bobot
     */
    public function test_AHP02_jumlah_bobot_adalah_4()
    {
        $result = $this->decision->hitungAHP();

        $this->assertCount(4, $result['bobot']);
    }

    /**
     * [AHP-03]
     * Path  : Verifikasi total bobot = 1 (sifat dasar AHP)
     * Branch: Hasil normalisasi kolom lalu rata-rata per baris
     */
    public function test_AHP03_total_bobot_sama_dengan_satu()
    {
        $result = $this->decision->hitungAHP();

        $totalBobot = array_sum($result['bobot']);

        $this->assertEqualsWithDelta(1.0, $totalBobot, 0.001,
            "Total bobot AHP harus = 1, didapat: $totalBobot"
        );
    }

    /**
     * [AHP-04]
     * Path  : Verifikasi setiap bobot bernilai positif (0 < bobot < 1)
     * Branch: Hasil bagi normalisasi tidak boleh negatif
     */
    public function test_AHP04_setiap_bobot_positif_dan_kurang_dari_satu()
    {
        $result = $this->decision->hitungAHP();

        foreach ($result['bobot'] as $i => $b) {
            $this->assertGreaterThan(0, $b,
                "Bobot ke-$i harus > 0, didapat: $b"
            );
            $this->assertLessThan(1, $b,
                "Bobot ke-$i harus < 1, didapat: $b"
            );
        }
    }

    /**
     * [AHP-05]
     * Path  : Verifikasi urutan bobot (C1 > C2 > C3 > C4)
     * Branch: Matrix diisi dominasi C1 > C2 > C3 > C4,
     *         bobot harus konsisten dengan pairwise comparison
     */
    public function test_AHP05_urutan_bobot_sesuai_prioritas_kriteria()
    {
        $result = $this->decision->hitungAHP();
        $b      = $result['bobot'];

        // Tingkat Serangan > Luas Lahan > Lama Kejadian > Jenis Hama
        $this->assertGreaterThan($b[1], $b[0], 'Bobot C1 harus > C2');
        $this->assertGreaterThan($b[2], $b[1], 'Bobot C2 harus > C3');
        $this->assertGreaterThan($b[3], $b[2], 'Bobot C3 harus > C4');
    }

    /**
     * [AHP-06]
     * Path  : Verifikasi nilai bobot mendekati nilai teoritis
     * Branch: Kalkulasi lambdaMax, CI, dan CR
     */
    public function test_AHP06_nilai_bobot_mendekati_nilai_teoritis()
    {
        $result = $this->decision->hitungAHP();
        $b      = $result['bobot'];

        $this->assertEqualsWithDelta(0.558, $b[0], 0.02, 'Bobot C1 (Tingkat Serangan)');
        $this->assertEqualsWithDelta(0.263, $b[1], 0.02, 'Bobot C2 (Luas Lahan)');
        $this->assertEqualsWithDelta(0.116, $b[2], 0.02, 'Bobot C3 (Lama Kejadian)');
        $this->assertEqualsWithDelta(0.063, $b[3], 0.02, 'Bobot C4 (Jenis Hama)');
    }

    /**
     * [AHP-07]
     * Path  : Verifikasi Consistency Ratio (CR) < 0.1
     * Branch: CR = CI / RI, RI = 0.90 untuk n=4
     *         Jika CR >= 0.1, matrix tidak konsisten
     */
    public function test_AHP07_CR_kurang_dari_0_1_konsisten()
    {
        $result = $this->decision->hitungAHP();

        $this->assertLessThan(0.1, $result['CR'],
            "CR harus < 0.1 (konsisten). CR = {$result['CR']}"
        );
    }

    /**
     * [AHP-08]
     * Path  : Verifikasi CR bertipe float/numerik
     * Branch: Hasil pembagian CI/RI tidak null dan numerik
     */
    public function test_AHP08_CR_bertipe_numerik()
    {
        $result = $this->decision->hitungAHP();

        $this->assertIsFloat($result['CR']);
    }

    // ============================================================
    // KELOMPOK 2 — TOPSIS — Branch: data < 2 (early return)
    // ============================================================

    /**
     * [TOPSIS-01]
     * Path  : Branch `if($data->count() < 2) return;` — data = 0
     * Branch: Data kosong → fungsi harus return null/void
     */
    public function test_TOPSIS01_data_kosong_return_null()
    {
        // Tidak ada data sama sekali
        $result = $this->decision->hitungTOPSIS();

        $this->assertTrue(
            $result === null || $result === [] || empty($result),
            'hitungTOPSIS() harus return null/kosong jika data < 2'
        );
    }

    /**
     * [TOPSIS-02]
     * Path  : Branch `if($data->count() < 2) return;` — data = 1
     * Branch: Hanya 1 data → harus early return, tidak proses TOPSIS
     */
    public function test_TOPSIS02_satu_data_return_null()
    {
        $this->buatLapor(); // hanya 1

        $result = $this->decision->hitungTOPSIS();

        $this->assertTrue(
            $result === null || $result === [] || empty($result),
            'hitungTOPSIS() harus return null/kosong jika data = 1'
        );
    }

    /**
     * [TOPSIS-03]
     * Path  : Branch data = 2 (batas minimal agar TOPSIS berjalan)
     * Branch: Count tepat 2 → harus masuk proses TOPSIS
     */
    public function test_TOPSIS03_dua_data_topsis_berjalan()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $this->decision->hitungTOPSIS();

        $lapor = Lapor::first();

        $this->assertNotNull($lapor->nilai_preferensi,
            'nilai_preferensi harus terisi jika data >= 2'
        );
    }

    // ============================================================
    // KELOMPOK 3 — TOPSIS — Semua cabang mapping kriteria
    // ============================================================

    /**
     * [TOPSIS-04]
     * Path  : Semua nilai $rSerangan dipakai (Ringan, Sedang, Berat)
     * Branch: Mapping array untuk Tingkat Serangan
     */
    public function test_TOPSIS04_semua_cabang_rSerangan()
    {
        $this->buatLapor(['tingkat_serangan' => 'Ringan', 'nama_petani' => 'P1']);
        $this->buatLapor(['tingkat_serangan' => 'Sedang', 'nama_petani' => 'P2',
            'jenis_hama' => 'Tikus']);
        $this->buatLapor(['tingkat_serangan' => 'Berat',  'nama_petani' => 'P3',
            'jenis_hama' => 'Penggerek Batang']);

        $this->decision->hitungTOPSIS();

        $laporan = Lapor::all();

        foreach ($laporan as $item) {
            $this->assertNotNull($item->nilai_preferensi);
        }
    }

    /**
     * [TOPSIS-05]
     * Path  : Semua nilai $rLuas dipakai (luas_kecil, luas_sedang, luas_besar)
     * Branch: Mapping array untuk Luas Lahan
     */
    public function test_TOPSIS05_semua_cabang_rLuas()
    {
        $this->buatLapor(['luas_lahan' => 'luas_kecil',  'nama_petani' => 'P1',
            'tingkat_serangan' => 'Ringan']);
        $this->buatLapor(['luas_lahan' => 'luas_sedang', 'nama_petani' => 'P2',
            'jenis_hama' => 'Tikus', 'tingkat_serangan' => 'Sedang']);
        $this->buatLapor(['luas_lahan' => 'luas_besar',  'nama_petani' => 'P3',
            'jenis_hama' => 'Penggerek Batang', 'tingkat_serangan' => 'Berat']);

        $this->decision->hitungTOPSIS();

        foreach (Lapor::all() as $item) {
            $this->assertNotNull($item->nilai_preferensi);
        }
    }

    /**
     * [TOPSIS-06]
     * Path  : Semua nilai $rLama dipakai (cepat, menengah, lama)
     * Branch: Mapping array untuk Lama Kejadian
     */
    public function test_TOPSIS06_semua_cabang_rLama()
    {
        $this->buatLapor(['lama_kejadian' => 'cepat',    'nama_petani' => 'P1',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil']);
        $this->buatLapor(['lama_kejadian' => 'menengah', 'nama_petani' => 'P2',
            'jenis_hama' => 'Tikus', 'tingkat_serangan' => 'Sedang']);
        $this->buatLapor(['lama_kejadian' => 'lama',     'nama_petani' => 'P3',
            'jenis_hama' => 'Penggerek Batang', 'tingkat_serangan' => 'Berat']);

        $this->decision->hitungTOPSIS();

        foreach (Lapor::all() as $item) {
            $this->assertNotNull($item->nilai_preferensi);
        }
    }

    /**
     * [TOPSIS-07]
     * Path  : Semua nilai $rHama dipakai
     *         (Wereng, Penggerek Batang, Tikus, Walang Sangit)
     * Branch: Mapping array untuk Jenis Hama
     */
    public function test_TOPSIS07_semua_cabang_rHama()
    {
        $this->buatLapor(['jenis_hama' => 'Wereng',           'nama_petani' => 'P1']);
        $this->buatLapor(['jenis_hama' => 'Penggerek Batang', 'nama_petani' => 'P2',
            'tingkat_serangan' => 'Sedang']);
        $this->buatLapor(['jenis_hama' => 'Tikus',            'nama_petani' => 'P3',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil']);
        $this->buatLapor(['jenis_hama' => 'Walang Sangit',    'nama_petani' => 'P4',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_sedang',
            'lama_kejadian' => 'cepat']);

        $this->decision->hitungTOPSIS();

        foreach (Lapor::all() as $item) {
            $this->assertNotNull($item->nilai_preferensi,
                "Nilai preferensi kosong untuk hama: {$item->jenis_hama}"
            );
        }
    }

    // ============================================================
    // KELOMPOK 4 — TOPSIS — Filter status (whereIn)
    // ============================================================

    /**
     * [TOPSIS-08]
     * Path  : Branch filter status — data berstatus 'ditolak' harus diabaikan
     * Branch: whereIn('status',['menunggu','diproses','selesai'])
     */
    public function test_TOPSIS08_data_ditolak_diabaikan()
    {
        $this->buatLapor(['status' => 'menunggu', 'nama_petani' => 'Valid1']);
        $this->buatLapor(['status' => 'diproses', 'nama_petani' => 'Valid2',
            'jenis_hama' => 'Tikus']);
        $this->buatLapor(['status' => 'ditolak',  'nama_petani' => 'Ditolak',
            'jenis_hama' => 'Walang Sangit']);

        $this->decision->hitungTOPSIS();

        $ditolak = Lapor::where('status', 'ditolak')->first();

        // Data ditolak tidak boleh dapat nilai preferensi
        $this->assertNull($ditolak->nilai_preferensi,
            'Data dengan status ditolak tidak boleh ikut kalkulasi TOPSIS'
        );
    }

    /**
     * [TOPSIS-09]
     * Path  : Semua status valid (menunggu, diproses, selesai) ikut kalkulasi
     * Branch: whereIn mencakup ketiga status
     */
    public function test_TOPSIS09_semua_status_valid_ikut_kalkulasi()
    {
        $this->buatLapor(['status' => 'menunggu', 'nama_petani' => 'P1']);
        $this->buatLapor(['status' => 'diproses', 'nama_petani' => 'P2',
            'jenis_hama' => 'Tikus', 'tingkat_serangan' => 'Sedang']);
        $this->buatLapor(['status' => 'selesai',  'nama_petani' => 'P3',
            'jenis_hama' => 'Penggerek Batang', 'tingkat_serangan' => 'Ringan',
            'luas_lahan' => 'luas_kecil']);

        $this->decision->hitungTOPSIS();

        $laporan = Lapor::whereIn('status', ['menunggu', 'diproses', 'selesai'])->get();

        foreach ($laporan as $item) {
            $this->assertNotNull($item->nilai_preferensi,
                "Status '{$item->status}' harus dapat nilai preferensi"
            );
        }
    }

    // ============================================================
    // KELOMPOK 5 — TOPSIS — Validasi nilai matematis
    // ============================================================

    /**
     * [TOPSIS-10]
     * Path  : Nilai preferensi harus berada di rentang [0, 1]
     * Branch: Formula TOPSIS V = D- / (D+ + D-)
     */
    public function test_TOPSIS10_nilai_preferensi_rentang_0_sampai_1()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $this->decision->hitungTOPSIS();

        foreach (Lapor::all() as $item) {
            $this->assertGreaterThanOrEqual(0, $item->nilai_preferensi,
                "Nilai preferensi {$item->nama_petani} tidak boleh < 0"
            );
            $this->assertLessThanOrEqual(1, $item->nilai_preferensi,
                "Nilai preferensi {$item->nama_petani} tidak boleh > 1"
            );
        }
    }

    /**
     * [TOPSIS-11]
     * Path  : Ranking tersimpan ke database setelah hitungTOPSIS()
     * Branch: Loop update ranking ($i+1)
     */
    public function test_TOPSIS11_ranking_tersimpan_ke_database()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang']);

        $this->decision->hitungTOPSIS();

        foreach (Lapor::all() as $item) {
            $this->assertNotNull($item->ranking,
                "Ranking {$item->nama_petani} tidak boleh null"
            );
        }
    }

    /**
     * [TOPSIS-12]
     * Path  : Ranking dimulai dari 1 (tidak dari 0)
     * Branch: $rank dimulai dari $i+1 bukan $i
     */
    public function test_TOPSIS12_ranking_dimulai_dari_satu()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Ringan']);

        $this->decision->hitungTOPSIS();

        $minRanking = Lapor::min('ranking');

        $this->assertEquals(1, $minRanking,
            "Ranking terkecil harus = 1, didapat: $minRanking"
        );
    }

    /**
     * [TOPSIS-13]
     * Path  : Ranking tidak duplikat — setiap laporan punya rank unik
     * Branch: orderByDesc('nilai_preferensi') lalu update berurutan
     */
    public function test_TOPSIS13_ranking_tidak_duplikat()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang']);
        $this->buatLapor(['nama_petani' => 'C', 'jenis_hama' => 'Walang Sangit',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $this->decision->hitungTOPSIS();

        $rankings = Lapor::pluck('ranking')->toArray();

        $this->assertEquals(
            count($rankings),
            count(array_unique($rankings)),
            'Setiap laporan harus memiliki ranking yang berbeda (unik)'
        );
    }

    /**
     * [TOPSIS-14]
     * Path  : Laporan ranking=1 memiliki nilai_preferensi tertinggi
     * Branch: orderByDesc('nilai_preferensi') menentukan ranking=1
     */
    public function test_TOPSIS14_ranking_satu_punya_nilai_preferensi_tertinggi()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $this->decision->hitungTOPSIS();

        $rank1     = Lapor::where('ranking', 1)->first();
        $maxNilai  = Lapor::max('nilai_preferensi');

        $this->assertEqualsWithDelta($maxNilai, $rank1->nilai_preferensi, 0.0001,
            'Laporan ranking=1 harus memiliki nilai_preferensi tertinggi'
        );
    }

    /**
     * [TOPSIS-15]
     * Path  : Urutan ranking sesuai nilai preferensi descending
     * Branch: orderByDesc() → ranking sequential
     */
    public function test_TOPSIS15_urutan_ranking_sesuai_nilai_preferensi()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang', 'luas_lahan' => 'luas_sedang']);
        $this->buatLapor(['nama_petani' => 'C', 'jenis_hama' => 'Walang Sangit',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $this->decision->hitungTOPSIS();

        $laporan = Lapor::orderBy('ranking')->get();

        for ($i = 0; $i < count($laporan) - 1; $i++) {
            $this->assertGreaterThanOrEqual(
                $laporan[$i + 1]->nilai_preferensi,
                $laporan[$i]->nilai_preferensi,
                "Ranking {$laporan[$i]->ranking} harus punya nilai_preferensi >= ranking berikutnya"
            );
        }
    }

    /**
     * [TOPSIS-16]
     * Path  : Ranking maksimal = jumlah data yang diproses
     * Branch: Loop ranking berakhir di count($data)
     */
    public function test_TOPSIS16_ranking_maksimal_sama_dengan_jumlah_data()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang']);
        $this->buatLapor(['nama_petani' => 'C', 'jenis_hama' => 'Walang Sangit',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $this->decision->hitungTOPSIS();

        $jumlahData  = Lapor::whereIn('status', ['menunggu', 'diproses', 'selesai'])->count();
        $maxRanking  = Lapor::max('ranking');

        $this->assertEquals($jumlahData, $maxRanking,
            "Ranking maksimal ($maxRanking) harus = jumlah data ($jumlahData)"
        );
    }

    // ============================================================
    // KELOMPOK 6 — getDetailTOPSIS — Struktur return
    // ============================================================

    /**
     * [DETAIL-01]
     * Path  : Return array memiliki semua key yang dibutuhkan
     * Branch: compact() menggabungkan semua variabel
     */
    public function test_DETAIL01_return_memiliki_semua_key()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang']);

        $detail = $this->decision->getDetailTOPSIS();

        $this->assertArrayHasKey('matrix',   $detail);
        $this->assertArrayHasKey('norm',     $detail);
        $this->assertArrayHasKey('weighted', $detail);
        $this->assertArrayHasKey('idealPos', $detail);
        $this->assertArrayHasKey('idealNeg', $detail);
        $this->assertArrayHasKey('dPos',     $detail);
        $this->assertArrayHasKey('dNeg',     $detail);
    }

    /**
     * [DETAIL-02]
     * Path  : Matrix awal setiap elemen berada di rentang [0, 1]
     * Branch: Pembagian nilai kriteria / 5 menghasilkan nilai 0–1
     */
    public function test_DETAIL02_nilai_matrix_rentang_0_sampai_1()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Walang Sangit',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $detail = $this->decision->getDetailTOPSIS();

        foreach ($detail['matrix'] as $i => $row) {
            foreach ($row as $j => $val) {
                $this->assertGreaterThanOrEqual(0, $val,
                    "matrix[$i][$j] = $val tidak boleh < 0"
                );
                $this->assertLessThanOrEqual(1, $val,
                    "matrix[$i][$j] = $val tidak boleh > 1"
                );
            }
        }
    }

    /**
     * [DETAIL-03]
     * Path  : Normalisasi setiap elemen berada di rentang [0, 1]
     * Branch: Pembagian elemen / sqrt(sum kuadrat kolom)
     */
    public function test_DETAIL03_nilai_normalisasi_rentang_0_sampai_1()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $detail = $this->decision->getDetailTOPSIS();

        foreach ($detail['norm'] as $i => $row) {
            foreach ($row as $j => $val) {
                $this->assertGreaterThanOrEqual(0, $val,
                    "norm[$i][$j] = $val tidak boleh < 0"
                );
                $this->assertLessThanOrEqual(1, $val,
                    "norm[$i][$j] = $val tidak boleh > 1"
                );
            }
        }
    }

    /**
     * [DETAIL-04]
     * Path  : Weighted = norm × bobot
     * Branch: Loop nested $i dan $j pada matriks terbobot
     * Ekspektasi: Nilai weighted <= nilai norm (bobot adalah pengali < 1)
     */
    public function test_DETAIL04_weighted_lebih_kecil_dari_norm()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang', 'luas_lahan' => 'luas_sedang']);

        $detail = $this->decision->getDetailTOPSIS();

        foreach ($detail['norm'] as $i => $row) {
            foreach ($row as $j => $val) {
                $this->assertLessThanOrEqual(
                    $val + 0.0001,
                    $detail['weighted'][$i][$j],
                    "weighted[$i][$j] tidak boleh > norm[$i][$j] karena bobot < 1"
                );
            }
        }
    }

    /**
     * [DETAIL-05]
     * Path  : Ideal positif >= ideal negatif untuk setiap kolom
     * Branch: max($col) >= min($col) selalu berlaku
     */
    public function test_DETAIL05_ideal_positif_lebih_besar_dari_ideal_negatif()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $detail = $this->decision->getDetailTOPSIS();

        for ($j = 0; $j < 4; $j++) {
            $this->assertGreaterThanOrEqual(
                $detail['idealNeg'][$j],
                $detail['idealPos'][$j],
                "idealPos[$j] harus >= idealNeg[$j]"
            );
        }
    }

    /**
     * [DETAIL-06]
     * Path  : Ideal positif memiliki tepat 4 elemen (n=4 kriteria)
     * Branch: Loop for($j=0;$j<4;$j++) untuk idealPos dan idealNeg
     */
    public function test_DETAIL06_ideal_memiliki_4_elemen()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang']);

        $detail = $this->decision->getDetailTOPSIS();

        $this->assertCount(4, $detail['idealPos'],
            'idealPos harus memiliki 4 elemen (1 per kriteria)'
        );
        $this->assertCount(4, $detail['idealNeg'],
            'idealNeg harus memiliki 4 elemen (1 per kriteria)'
        );
    }

    /**
     * [DETAIL-07]
     * Path  : Jarak D+ dan D- tidak negatif (hasil sqrt selalu >= 0)
     * Branch: sqrt($dp) dan sqrt($dn)
     */
    public function test_DETAIL07_jarak_dPos_dan_dNeg_tidak_negatif()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $detail = $this->decision->getDetailTOPSIS();

        foreach ($detail['dPos'] as $i => $d) {
            $this->assertGreaterThanOrEqual(0, $d,
                "dPos[$i] = $d tidak boleh negatif"
            );
        }

        foreach ($detail['dNeg'] as $i => $d) {
            $this->assertGreaterThanOrEqual(0, $d,
                "dNeg[$i] = $d tidak boleh negatif"
            );
        }
    }

    /**
     * [DETAIL-08]
     * Path  : Jumlah baris matrix = jumlah data
     * Branch: foreach $data mengisi $matrix[]
     */
    public function test_DETAIL08_jumlah_baris_matrix_sama_dengan_jumlah_data()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang']);
        $this->buatLapor(['nama_petani' => 'C', 'jenis_hama' => 'Penggerek Batang',
            'tingkat_serangan' => 'Ringan', 'luas_lahan' => 'luas_kecil',
            'lama_kejadian' => 'cepat']);

        $detail      = $this->decision->getDetailTOPSIS();
        $jumlahData  = Lapor::whereIn('status', ['menunggu', 'diproses', 'selesai'])->count();

        $this->assertCount($jumlahData, $detail['matrix'],
            "Jumlah baris matrix ({$jumlahData}) harus sama dengan jumlah data"
        );
        $this->assertCount($jumlahData, $detail['norm']);
        $this->assertCount($jumlahData, $detail['weighted']);
        $this->assertCount($jumlahData, $detail['dPos']);
        $this->assertCount($jumlahData, $detail['dNeg']);
    }

    /**
     * [DETAIL-09]
     * Path  : Setiap baris matrix memiliki tepat 4 kolom (n=4 kriteria)
     * Branch: $matrix[] = [c1, c2, c3, c4]
     */
    public function test_DETAIL09_setiap_baris_matrix_memiliki_4_kolom()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang']);

        $detail = $this->decision->getDetailTOPSIS();

        foreach ($detail['matrix'] as $i => $row) {
            $this->assertCount(4, $row,
                "Baris matrix[$i] harus memiliki 4 kolom"
            );
        }

        foreach ($detail['norm'] as $i => $row) {
            $this->assertCount(4, $row,
                "Baris norm[$i] harus memiliki 4 kolom"
            );
        }

        foreach ($detail['weighted'] as $i => $row) {
            $this->assertCount(4, $row,
                "Baris weighted[$i] harus memiliki 4 kolom"
            );
        }
    }

    // ============================================================
    // KELOMPOK 7 — Integrasi AHP + TOPSIS
    // ============================================================

    /**
     * [INT-01]
     * Path  : Bobot AHP digunakan dalam TOPSIS (integrasi end-to-end)
     * Branch: $ahp = $this->hitungAHP() → $bobot = $ahp['bobot']
     * Verifikasi: weighted[i][j] = norm[i][j] × bobot[j]
     */
    public function test_INT01_bobot_AHP_dipakai_dalam_weighted_TOPSIS()
    {
        $this->buatLapor(['nama_petani' => 'A']);
        $this->buatLapor(['nama_petani' => 'B', 'jenis_hama' => 'Tikus',
            'tingkat_serangan' => 'Sedang', 'luas_lahan' => 'luas_sedang']);

        $ahp    = $this->decision->hitungAHP();
        $bobot  = $ahp['bobot'];
        $detail = $this->decision->getDetailTOPSIS();

        foreach ($detail['norm'] as $i => $row) {
            for ($j = 0; $j < 4; $j++) {
                $expected = $row[$j] * $bobot[$j];
                $actual   = $detail['weighted'][$i][$j];

                $this->assertEqualsWithDelta($expected, $actual, 0.0001,
                    "weighted[$i][$j] harus = norm[$i][$j] × bobot[$j]"
                );
            }
        }
    }

    /**
     * [INT-02]
     * Path  : Skenario data lengkap — semua kombinasi kriteria
     * Branch: Semua mapping kriteria digunakan dalam satu run
     */
    public function test_INT02_skenario_data_lengkap_semua_kombinasi_kriteria()
    {
        // Data mencakup semua branch mapping kriteria sekaligus
        $dataSet = [
            ['jenis_hama' => 'Wereng',           'tingkat_serangan' => 'Berat',
             'luas_lahan' => 'luas_besar',  'lama_kejadian' => 'lama',     'nama_petani' => 'P1'],
            ['jenis_hama' => 'Penggerek Batang', 'tingkat_serangan' => 'Sedang',
             'luas_lahan' => 'luas_sedang', 'lama_kejadian' => 'menengah', 'nama_petani' => 'P2'],
            ['jenis_hama' => 'Tikus',            'tingkat_serangan' => 'Ringan',
             'luas_lahan' => 'luas_kecil',  'lama_kejadian' => 'cepat',    'nama_petani' => 'P3'],
            ['jenis_hama' => 'Walang Sangit',    'tingkat_serangan' => 'Sedang',
             'luas_lahan' => 'luas_sedang', 'lama_kejadian' => 'menengah', 'nama_petani' => 'P4'],
        ];

        foreach ($dataSet as $d) {
            $this->buatLapor($d);
        }

        $this->decision->hitungTOPSIS();

        $laporan = Lapor::whereIn('status', ['menunggu', 'diproses', 'selesai'])->get();

        $this->assertCount(4, $laporan);

        foreach ($laporan as $item) {
            $this->assertNotNull($item->nilai_preferensi);
            $this->assertNotNull($item->ranking);
            $this->assertGreaterThanOrEqual(0, $item->nilai_preferensi);
            $this->assertLessThanOrEqual(1, $item->nilai_preferensi);
        }

        // Pastikan ranking 1–4 semua ada
        $rankings = Lapor::pluck('ranking')->sort()->values()->toArray();
        $this->assertEquals([1, 2, 3, 4], $rankings);
    }

    /**
     * [INT-03]
     * Path  : Data terbaik (semua kriteria max) mendapat ranking=1
     * Branch: Nilai preferensi tertinggi → ranking=1
     */
    public function test_INT03_data_terbaik_mendapat_ranking_satu()
    {
        // P1 = semua kriteria TERTINGGI (harusnya ranking 1)
        $terbaik = $this->buatLapor([
            'nama_petani'      => 'TerbaikP1',
            'jenis_hama'       => 'Wereng',     // nilai 5 (tertinggi)
            'tingkat_serangan' => 'Berat',       // nilai 5
            'luas_lahan'       => 'luas_besar',  // nilai 5
            'lama_kejadian'    => 'lama',         // nilai 5
        ]);

        // P2 = semua kriteria TERENDAH
        $this->buatLapor([
            'nama_petani'      => 'TerendahP2',
            'jenis_hama'       => 'Walang Sangit', // nilai 2 (terendah)
            'tingkat_serangan' => 'Ringan',          // nilai 1
            'luas_lahan'       => 'luas_kecil',      // nilai 1
            'lama_kejadian'    => 'cepat',            // nilai 1
        ]);

        $this->decision->hitungTOPSIS();

        $rank1 = Lapor::where('ranking', 1)->first();

        $this->assertEquals('TerbaikP1', $rank1->nama_petani,
            'Data dengan semua kriteria tertinggi harus mendapat ranking = 1'
        );
    }
}