/**
 * Copyright (c) 2025 SoftCorporation LLC. All rights reserved.
 *
 * The Software License, Version 1.0
 *
 * SoftCorporation LLC. grants you ("Licensee") a non-exclusive, royalty free,
 * license to use, modify and redistribute this software in source and binary
 * code form, provided that the following conditions are met:
 *
 * 1. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        SoftCorporation LLC. (http://www.softcorporation.com)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 2. This software and SoftCorporation name must not be used to
 *    promote products derived from the software without prior
 *    written permission. For written permission, please contact
 *    info@softcorporation.com.
 *
 * This software is provided "AS IS," without a warranty of any kind.
 * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED.
 * IN NO EVENT SHALL THE SOFTCORPORATION BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION).
 *
 */
package com.softcorporation.sandi.base.util;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

/**
 * Tests for HashService SimHash computation.
 *
 * Phrases are grouped to demonstrate expected Hamming distance behaviour:
 *   - identical phrases          → distance == 0
 *   - near-duplicate phrases     → small distance (typo, one word swapped)
 *   - unrelated phrases          → large distance
 */
class HashServiceTest {

    // ---------- 10 test phrases ----------

    private static final String P1  = "quick brown fox jumps";
    private static final String P2  = "quick brown fox jumps";        // identical to P1
    private static final String P3  = "quick brown fox leaps";        // one word different
    private static final String P4  = "quikc brown fox jumps";        // typo in first word
    private static final String P5  = "quick brown dog jumps";        // one word different (dog)
    private static final String P6  = "machine learning model";
    private static final String P7  = "machine lerning model";        // typo: lerning
    private static final String P8  = "brown quick jumps fox";
    private static final String P9  = "artificial intelligence system";
    private static final String P10 = "completely unrelated sentence"; // unrelated to P1

    // ---------- determinism ----------

//    @Test
    void testSamePhraseProducesSameHash() {
        assertEquals(HashService.simHash(P1), HashService.simHash(P2));
    }

//    @Test
    void testHashIsDeterministic() {
        int h1 = HashService.simHash(P1);
        int h2 = HashService.simHash(P1);
        int h3 = HashService.simHash(P1);
        assertEquals(h1, h2);
        assertEquals(h2, h3);
    }

    // ---------- identical → distance 0 ----------

//    @Test
    void testIdenticalPhrasesHaveZeroHammingDistance() {
        int h1 = HashService.simHash(P1);
        int h2 = HashService.simHash(P2);
        assertEquals(0, HashService.hammingDistance(h1, h2));
    }

    // ---------- near-duplicates → small distance ----------

//    @Test
    void testOneWordDifferentHasSmallHammingDistance() {
        // "jumps" vs "leaps" — only the last word differs
        int h1 = HashService.simHash(P1);
        int h3 = HashService.simHash(P3);
        int distance = HashService.hammingDistance(h1, h3);
        System.out.println("P1 vs P3 (one word diff): " + distance);
        assertTrue(distance <= 12, "Expected small distance for one-word change, got: " + distance);
    }

//    @Test
    void testOneWordDifferentHasSmallHammingDistance2() {
        // "jumps" vs "leaps" — only the last word differs
        long h1 = HashService.simHash2(P1, true);
        long h3 = HashService.simHash2(P3, false);
        int distance = HashService.hammingDistance2(h1, h3);
        System.out.println("2: P1 vs P3 (one word diff): " + distance);
        assertTrue(distance <= 12, "2: Expected small distance for one-word change, got: " + distance);
    }

//    @Test
    void testTypoInWordHasSmallHammingDistance() {
        // "quick" vs "quikc" — char n-grams make this detectable
        int h1 = HashService.simHash(P1);
        int h4 = HashService.simHash(P4);
        int distance = HashService.hammingDistance(h1, h4);
        System.out.println("P1 vs P4 (typo): " + distance);
        assertTrue(distance <= 12, "Expected small distance for typo, got: " + distance);
    }

//    @Test
    void testMachinelearningTypoHasSmallHammingDistance() {
        // "learning" vs "lerning"
        int h6 = HashService.simHash(P6);
        int h7 = HashService.simHash(P7);
        int distance = HashService.hammingDistance(h6, h7);
        System.out.println("P6 vs P7 (typo in learning): " + distance);
        assertTrue(distance <= 12, "Expected small distance for typo in learning, got: " + distance);
    }

//    @Test
    void testOneWordSwappedHasSmallerDistanceThanUnrelated() {
        // P1 vs P5 (dog instead of fox) should be closer than P1 vs P10
        int h1  = HashService.simHash(P1);
        int h5  = HashService.simHash(P5);
        int h10 = HashService.simHash(P10);
        int distNear = HashService.hammingDistance(h1, h5);
        int distFar  = HashService.hammingDistance(h1, h10);
        System.out.println("P1 vs P5 (near): " + distNear + "  P1 vs P10 (far): " + distFar);
        assertTrue(distNear < distFar, "Near-duplicate should be closer than unrelated phrase");
    }

    // ---------- unrelated phrases → large distance ----------

//    @Test
    void testUnrelatedPhrasesHaveLargeHammingDistance() {
        int h1  = HashService.simHash(P1);
        int h10 = HashService.simHash(P10);
        int distance = HashService.hammingDistance(h1, h10);
        System.out.println("P1 vs P10 (unrelated): " + distance);
        assertTrue(distance >= 8, "Expected large distance for unrelated phrases, got: " + distance);
    }

//    @Test
    void testDifferentDomainPhrasesHaveLargeHammingDistance() {
        int h8 = HashService.simHash(P8);
        int h9 = HashService.simHash(P9);
        int distance = HashService.hammingDistance(h8, h9);
        System.out.println("P8 vs P9 (different domain): " + distance);
        assertTrue(distance >= 6, "Expected large distance for different domain phrases, got: " + distance);
    }

//    @Test
    void testDifferentWordOrderHammingDistance() {
        int h1 = HashService.simHash(P1);
        int h8 = HashService.simHash(P8);
        int distance = HashService.hammingDistance(h1, h8);
        System.out.println("P1 vs P8 (different word order): " + distance);
        assertTrue(distance <= 6, "Expected small distance, got: " + distance);
    }

    // ---------- edge cases ----------

//    @Test
    void testNullReturnsZero() {
        assertEquals(0, HashService.simHash(null));
    }

//    @Test
    void testEmptyStringReturnsZero() {
        assertEquals(0, HashService.simHash(""));
    }

//    @Test
    void testCaseInsensitive() {
        int lower = HashService.simHash("Quick Brown Fox");
        int upper = HashService.simHash("quick brown fox");
        assertEquals(lower, upper, "SimHash should be case-insensitive");
    }

    // ---------- bands ----------

//    @Test
    void testBandsLength() {
        int hash = HashService.simHash(P1);
        int[] bands = HashService.bands(hash, 4);
        assertEquals(4, bands.length);
    }

//    @Test
    void testBandsReconstructHash() {
        int hash = HashService.simHash(P6);
        int[] bands = HashService.bands(hash, 4); // 4 bands × 8 bits
        int reconstructed = bands[0] | (bands[1] << 8) | (bands[2] << 16) | (bands[3] << 24);
        assertEquals(hash, reconstructed, "Bands should reconstruct the original hash");
    }

    // ---------- summary print ----------

//    @Test
    void testPrintAllPhraseHashes() {
        String[] phrases = { P1, P2, P3, P4, P5, P6, P7, P8, P9, P10 };
        System.out.println("\n--- SimHash values ---");
        for (int i = 0; i < phrases.length; i++) {
            int h = HashService.simHash(phrases[i]);
            System.out.printf("P%-2d  hash=%08x  bits=%s  phrase=\"%s\"%n",
                i + 1,
                h,
                String.format("%32s", Integer.toBinaryString(h)).replace(' ', '0'),
                phrases[i]);
        }
        System.out.println("\n--- Hamming distances (selected pairs) ---");
        int[][] pairs = { {0,1},{0,2},{0,3},{0,4},{5,6},{7,8},{0,9} };
        for (int[] pair : pairs) {
            int d = HashService.hammingDistance(
                HashService.simHash(phrases[pair[0]]),
                HashService.simHash(phrases[pair[1]]));
            System.out.printf("P%d vs P%d → %d%n", pair[0]+1, pair[1]+1, d);
        }
    }


    @Test
    void testPrintAllPhraseHashes2() {
        String[] phrases = { P1, P3 };
        System.out.println("\n--- 2: SimHash values ---");
        for (int i = 0; i < phrases.length; i++) {
            long h = HashService.simHash2(phrases[i], false);
            System.out.printf("P%-2d  hash=%08x  bits=%s  phrase=\"%s\"%n",
                i + 1,
                h,
                String.format("%64s", Long.toBinaryString(h)).replace(' ', '0'),
                phrases[i]);
        }
        System.out.println("\n--- 2: Hamming distances (selected pairs) ---");
        long h1 = HashService.simHash2(P1, true);
        long h2 = HashService.simHash2(P3, false);
        printBits(h1);
        printBits(h2);

        int d = HashService.hammingDistance2(h1, h2);
        System.out.printf("1 vs 3 → %d%n", d);
        System.out.println("match: " + HashService.hammingMatch(h1, h2));
    }

    private void printBits(final long h) {
    	System.out.printf("bits=%s\n", String.format("%64s", Long.toBinaryString(h)).replace(' ', '0'));
    }
}
