<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\library\version_control\GitDelta;
use seekquarry\yioop\library\UnitTest;

/**
 * Checks that the short description of how two versions of some content
 * differ can be read back to give exactly the newer version, for text, for
 * binary content, and for the awkward cases at the edges of the format.
 *
 * @author Chris Pollett
 */
class GitDeltaTest extends UnitTest
{
    /**
     * Nothing needs standing up: the maker and applier work on strings
     * handed to them, touching neither disk nor database.
     */
    public function setUp()
    {
    }
    /**
     * Nothing was stood up, so nothing needs taking down.
     */
    public function tearDown()
    {
    }
    /**
     * A version rebuilt from a delta is exactly the version it was made
     * from, whether the change is an edit in the middle, an addition at the
     * end, or a truncation.
     */
    public function textRoundTripTestCase()
    {
        $source = str_repeat("a line of ordinary source code\n", 120);
        $edited = substr($source, 0, 1500) . "an inserted line\n" .
            substr($source, 1500);
        $appended = $source . "a line added at the end\n";
        $shortened = substr($source, 0, 900);
        $versions = ["edited" => $edited, "appended" => $appended,
            "shortened" => $shortened];
        foreach ($versions as $name => $target) {
            $delta = GitDelta::create($source, $target);
            $this->assertTrue($delta !== null,
                "a delta is worth making for the $name version");
            $this->assertEqual(GitDelta::apply($source, $delta), $target,
                "the $name version is rebuilt exactly");
        }
    }
    /**
     * Content that is not text rebuilds exactly too, since the description
     * is written in terms of bytes rather than lines.
     */
    public function binaryRoundTripTestCase()
    {
        $source = "";
        for ($i = 0; $i < 8000; $i++) {
            $source .= chr($i % 256);
        }
        $target = substr($source, 0, 3000) . "\x00\x01\xff\xfe" .
            substr($source, 3000);
        $delta = GitDelta::create($source, $target);
        $this->assertTrue($delta !== null,
            "a delta is worth making for changed binary content");
        $this->assertEqual(GitDelta::apply($source, $delta), $target,
            "binary content is rebuilt exactly");
        $this->assertTrue(strlen($delta) < strlen($target) / 10,
            "the delta is much smaller than the content it describes");
    }
    /**
     * Two versions with nothing in common are not worth describing as a
     * difference, so no delta is made and the caller sends the content
     * whole.
     */
    public function unrelatedContentDeclinedTestCase()
    {
        $source = str_repeat("aaaabbbbccccdddd", 200);
        $target = "";
        for ($i = 0; $i < 3200; $i++) {
            $target .= chr(($i * 7 + 13) % 256);
        }
        $delta = GitDelta::create($source, $target);
        $this->assertTrue($delta === null,
            "unrelated content is sent whole rather than as a delta");
    }
    /**
     * An empty version on either side is sent whole, there being nothing to
     * describe a difference against.
     */
    public function emptyContentDeclinedTestCase()
    {
        $this->assertTrue(GitDelta::create("", "something") === null,
            "nothing to build against means no delta");
        $this->assertTrue(GitDelta::create("something", "") === null,
            "nothing to build means no delta");
    }
    /**
     * A version identical to the one before it is described by copy steps
     * alone, so the delta is a few bytes whatever the content's length.
     */
    public function identicalContentTestCase()
    {
        $source = str_repeat("unchanged content\n", 300);
        $delta = GitDelta::create($source, $source);
        $this->assertTrue($delta !== null,
            "an unchanged version is worth describing as a delta");
        $this->assertTrue(strlen($delta) < 50,
            "an unchanged version needs only a few bytes to describe");
        $this->assertEqual(GitDelta::apply($source, $delta), $source,
            "an unchanged version is rebuilt exactly");
    }
    /**
     * A damaged description is refused rather than acted on: one whose copy
     * step reaches past the end of the version it is built against, and one
     * that does not produce the length it declares.
     */
    public function damagedDeltaRefusedTestCase()
    {
        $source = str_repeat("source content\n", 100);
        $delta = GitDelta::create($source, $source . "tail\n");
        $damaged = substr($delta, 0, strlen($delta) - 3);
        $refused = false;
        try {
            GitDelta::apply($source, $damaged);
        } catch (\Exception $exception) {
            $refused = true;
        }
        $this->assertTrue($refused,
            "a delta cut short is refused rather than acted on");
        $refused = false;
        try {
            GitDelta::apply("short", $delta);
        } catch (\Exception $exception) {
            $refused = true;
        }
        $this->assertTrue($refused,
            "a delta built against other content is refused");
    }
}
