<?php

namespace Flow\JSONPath\Test;

require_once __DIR__ . "/../vendor/autoload.php";

use Flow\JSONPath\JSONPath;
use PHPUnit\Framework\TestCase;

class JSONPathTest extends TestCase
{

    /**
     * $.store.books[0].title
     */
    public function testChildOperators()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find('$.store.books[0].title');
        $this->assertEquals('Sayings of the Century', $result[0]);
    }

    /**
     * $['store']['books'][0]['title']
     */
    public function testChildOperatorsAlt()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$['store']['books'][0]['title']");
        $this->assertEquals('Sayings of the Century', $result[0]);
    }

    /**
     * $.array[start:end:step]
     */
    public function testFilterSliceA()
    {
        // Copy all items... similar to a wildcard
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$['store']['books'][:].title");
        $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick', 'The Lord of the Rings'], $result->data());
    }

    /**
     * Positive end indexes
     * $[0:2]
     */
    public function testFilterSlice_PositiveEndIndexes()
    {
        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:0]");
        $this->assertEquals([], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:1]");
        $this->assertEquals(["first"], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:2]");
        $this->assertEquals(["first", "second"], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:2]");
        $this->assertEquals(["first", "second"], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[1:2]");
        $this->assertEquals(["second"], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:3:1]");
        $this->assertEquals(["first", "second","third"], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:3:0]");
        $this->assertEquals(["first", "second","third"], $result->data());
    }

    public function testFilterSlice_NegativeStartIndexes()
    {
        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:]");
        $this->assertEquals(["fourth", "fifth"], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-1:]");
        $this->assertEquals(["fifth"], $result->data());
    }

    /**
     * Negative end indexes
     * $[:-2]
     */
    public function testFilterSlice_NegativeEndIndexes()
    {
        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:-2]");
        $this->assertEquals(["first", "second", "third"], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:-2]");
        $this->assertEquals(["first", "second", "third"], $result->data());
    }

    /**
     * Negative end indexes
     * $[:-2]
     */
    public function testFilterSlice_NegativeStartAndEndIndexes()
    {
        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:-1]");
        $this->assertEquals(["fourth"], $result->data());

        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-4:-2]");
        $this->assertEquals(["second", "third"], $result->data());
    }

    /**
     * Negative end indexes
     * $[:-2]
     */
    public function testFilterSlice_NegativeStartAndPositiveEnd()
    {
        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:2]");
        $this->assertEquals([], $result->data());
    }

    public function testFilterSlice_StepBy2()
    {
        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:4:2]");
        $this->assertEquals(["first", "third"], $result->data());
    }


    /**
     * The Last item
     * $[-1]
     */
    public function testFilterLastIndex()
    {
        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-1]");
        $this->assertEquals(["fifth"], $result->data());
    }

    /**
     * Array index slice only end
     * $[:2]
     */
    public function testFilterSliceG()
    {
        // Fetch up to the second index
        $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:2]");
        $this->assertEquals(["first", "second"], $result->data());
    }

    /**
     * $.store.books[(@.length-1)].title
     *
     * This notation is only partially implemented eg. hacked in
     */
    public function testChildQuery()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[(@.length-1)].title");
        $this->assertEquals(['The Lord of the Rings'], $result->data());
    }

    /**
     * $.store.books[?(@.price < 10)].title
     * Filter books that have a price less than 10
     */
    public function testQueryMatchLessThan()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[?(@.price < 10)].title");
        $this->assertEquals(['Sayings of the Century', 'Moby Dick'], $result->data());
    }

    /**
     * $..books[?(@.author == "J. R. R. Tolkien")]
     * Filter books that have a title equal to "..."
     */
    public function testQueryMatchEquals()
    {
        $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author == "J. R. R. Tolkien")].title');
        $this->assertEquals($results[0], 'The Lord of the Rings');
    }

    /**
     * $..books[?(@.author = 1)]
     * Filter books that have a title equal to "..."
     */
    public function testQueryMatchEqualsWithUnquotedInteger()
    {
        $results = (new JSONPath($this->exampleDataWithSimpleIntegers(rand(0, 1))))->find('$..features[?(@.value = 1)]');
        $this->assertEquals($results[0]->name, "foo");
        $this->assertEquals($results[1]->name, "baz");
    }

    /**
     * $..books[?(@.author != "J. R. R. Tolkien")]
     * Filter books that have a title not equal to "..."
     */
    public function testQueryMatchNotEqualsTo()
    {
        $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author != "J. R. R. Tolkien")].title');
        $this->assertcount(3, $results);
        $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);

        $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author !== "J. R. R. Tolkien")].title');
        $this->assertcount(3, $results);
        $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);

        $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author <> "J. R. R. Tolkien")].title');
        $this->assertcount(3, $results);
        $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);
    }

    /**
     * $.store.books[*].author
     */
    public function testWildcardAltNotation()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[*].author");
        $this->assertEquals(['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'], $result->data());
    }

    /**
     * $..author
     */
    public function testRecursiveChildSearch()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..author");
        $this->assertEquals(['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'], $result->data());
    }

    /**
     * $.store.*
     * all things in store
     * the structure of the example data makes this test look weird
     */
    public function testWildCard()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.*");
        if (is_object($result[0][0])) {
            $this->assertEquals('Sayings of the Century', $result[0][0]->title);
        } else {
            $this->assertEquals('Sayings of the Century', $result[0][0]['title']);
        }

        if (is_object($result[1])) {
            $this->assertEquals('red', $result[1]->color);
        } else {
            $this->assertEquals('red', $result[1]['color']);
        }
    }

    /**
     * $.store..price
     * the price of everything in the store.
     */
    public function testRecursiveChildSearchAlt()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store..price");
        $this->assertEquals([8.95, 12.99, 8.99, 22.99, 19.95], $result->data());
    }

    /**
     * $..books[2]
     * the third book
     */
    public function testRecursiveChildSearchWithChildIndex()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[2].title");
        $this->assertEquals(["Moby Dick"], $result->data());
    }

    /**
     * $..books[(@.length-1)]
     */
    public function testRecursiveChildSearchWithChildQuery()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[(@.length-1)].title");
        $this->assertEquals(["The Lord of the Rings"], $result->data());
    }

    /**
     * $..books[-1:]
     * Resturn the last results
     */
    public function testRecursiveChildSearchWithSliceFilter()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[-1:].title");
        $this->assertEquals(["The Lord of the Rings"], $result->data());
    }

    /**
     * $..books[?(@.isbn)]
     * filter all books with isbn number
     */
    public function testRecursiveWithQueryMatch()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[?(@.isbn)].isbn");

        $this->assertEquals(['0-553-21311-3', '0-395-19395-8'], $result->data());
    }

    /**
     * $..*
     * All members of JSON structure
     */
    public function testRecursiveWithWildcard()
    {
        $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..*");
        $result = json_decode(json_encode($result), true);

        $this->assertEquals('Sayings of the Century', $result[0]['books'][0]['title']);
        $this->assertEquals(19.95, $result[26]);
    }

    /**
     * Tests direct key access.
     */
    public function testSimpleArrayAccess()
    {
        $result = (new JSONPath(['title' => 'test title']))->find('title');

        $this->assertEquals(['test title'], $result->data());
    }

    public function testFilteringOnNoneArrays()
    {
        $data = ['foo' => 'asdf'];

        $result = (new JSONPath($data))->find("$.foo.bar");
        $this->assertEquals([], $result->data());
    }


    public function testMagicMethods()
    {
        $fooClass = new JSONPathTestClass();

        $results = (new JSONPath($fooClass, JSONPath::ALLOW_MAGIC))->find('$.foo');

        $this->assertEquals(['bar'], $results->data());
    }


    public function testMatchWithComplexSquareBrackets()
    {
        $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'][?(@['@language']='en')]['@language']");
        $this->assertEquals(["en"], $result->data());
    }

    public function testQueryMatchWithRecursive()
    {
        $locations = $this->exampleDataLocations();
        $result    = (new JSONPath($locations))->find("..[?(@.type == 'suburb')].name");
        $this->assertEquals(["Rosebank"], $result->data());
    }

    public function testFirst()
    {
        $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'].*");

        $this->assertEquals(["@language" => "en"], $result->first()->data());
    }

    public function testLast()
    {
        $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'].*");
        $this->assertEquals(["@language" => "de"], $result->last()->data());
    }

    public function testSlashesInIndex()
    {
        $result = (new JSONPath($this->exampleDataWithSlashes()))->find("$['mediatypes']['image/png']");

        $this->assertEquals(
            [
                "/core/img/filetypes/image.png",
            ],
            $result->data()
        );
    }

    public function testCyrillicText()
    {
        $result = (new JSONPath(["трололо" => 1]))->find("$['трололо']");

        $this->assertEquals([1], $result->data());

        $result = (new JSONPath(["трололо" => 1]))->find("$.трололо");

        $this->assertEquals([1], $result->data());
    }

    public function testOffsetUnset()
    {
        $data = [
            "route" => [
                ["name" => "A", "type" => "type of A"],
                ["name" => "B", "type" => "type of B"],
            ],
        ];
        $data = json_encode($data);

        $jsonIterator = new JSONPath(json_decode($data));

        /** @var JSONPath $route */
        $route = $jsonIterator->offsetGet('route');

        $route->offsetUnset(0);

        $first = $route->first();

        $this->assertEquals("B", $first['name']);
    }


    public function testFirstKey()
    {
        // Array test for array
        $jsonPath = new JSONPath(['a' => 'A', 'b', 'B']);

        $firstKey = $jsonPath->firstKey();

        $this->assertEquals('a', $firstKey);

        // Array test for object
        $jsonPath = new JSONPath((object)['a' => 'A', 'b', 'B']);

        $firstKey = $jsonPath->firstKey();

        $this->assertEquals('a', $firstKey);
    }

    public function testLastKey()
    {
        // Array test for array
        $jsonPath = new JSONPath(['a' => 'A', 'b' => 'B', 'c' => 'C']);

        $lastKey = $jsonPath->lastKey();

        $this->assertEquals('c', $lastKey);

        // Array test for object
        $jsonPath = new JSONPath((object)['a' => 'A', 'b' => 'B', 'c' => 'C']);

        $lastKey = $jsonPath->lastKey();

        $this->assertEquals('c', $lastKey);
    }

    /**
     * Test: ensure trailing comma is stripped during parsing
     */
    public function testTrailingComma()
    {
        $jsonPath = new JSONPath(json_decode('{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}},"expensive":10}'));

        $result = $jsonPath->find("$..book[0,1,2,]");

        $this->assertCount(3, $result);
    }

    /**
     * Test: ensure negative indexes return -n from last index
     */
    public function testNegativeIndex()
    {
        $jsonPath = new JSONPath(json_decode('{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}},"expensive":10}'));

        $result = $jsonPath->find("$..book[-2]");

        $this->assertEquals("Herman Melville", $result[0]['author']);
    }

    public function testQueryAccessWithNumericalIndexes()
    {
        $jsonPath = new JSONPath(json_decode('{
            "result": {
                "list": [
                    {
                        "time": 1477526400,
                        "o": "11.51000"
                    },
                    {
                        "time": 1477612800,
                        "o": "11.49870"
                    }
                ]
            }
        }'));

        $result = $jsonPath->find("$.result.list[?(@.o == \"11.51000\")]");

        $this->assertEquals("11.51000", $result[0]->o);

        $jsonPath = new JSONPath(json_decode('{
            "result": {
                "list": [
                    [
                        1477526400,
                        "11.51000"
                    ],
                    [
                        1477612800,
                        "11.49870"
                    ]
                ]
            }
        }'));

        $result = $jsonPath->find("$.result.list[?(@[1] == \"11.51000\")]");

        $this->assertEquals("11.51000", $result[0][1]);

    }


    public function exampleData($asArray = true)
    {
        $json = '
        {
          "store":{
            "books":[
              {
                "category":"reference",
                "author":"Nigel Rees",
                "title":"Sayings of the Century",
                "price":8.95
              },
              {
                "category":"fiction",
                "author":"Evelyn Waugh",
                "title":"Sword of Honour",
                "price":12.99
              },
              {
                "category":"fiction",
                "author":"Herman Melville",
                "title":"Moby Dick",
                "isbn":"0-553-21311-3",
                "price":8.99
              },
              {
                "category":"fiction",
                "author":"J. R. R. Tolkien",
                "title":"The Lord of the Rings",
                "isbn":"0-395-19395-8",
                "price":22.99
              }
            ],
            "bicycle":{
              "color":"red",
              "price":19.95
            }
          }
        }';
        return json_decode($json, $asArray);
    }

    public function exampleDataExtra($asArray = true)
    {
        $json = '
            {
               "http://www.w3.org/2000/01/rdf-schema#label":[
                  {
                     "@language":"en"
                  },
                  {
                     "@language":"de"
                  }
               ]
            }
        ';

        return json_decode($json, $asArray);
    }


    public function exampleDataLocations($asArray = true)
    {
        $json = '
            {
               "name": "Gauteng",
               "type": "province",
               "child": {
                    "name": "Johannesburg",
                    "type": "city",
                    "child": {
                        "name": "Rosebank",
                        "type": "suburb"
                    }
               }
            }
        ';

        return json_decode($json, $asArray);
    }


    public function exampleDataWithSlashes($asArray = true)
    {
        $json = '
            {
                "features": [],
                "mediatypes": {
                    "image/png": "/core/img/filetypes/image.png",
                    "image/jpeg": "/core/img/filetypes/image.png",
                    "image/gif": "/core/img/filetypes/image.png",
                    "application/postscript": "/core/img/filetypes/image-vector.png"
                }
            }
        ';

        return json_decode($json, $asArray);
    }

    public function exampleDataWithSimpleIntegers($asArray = true)
    {
        $json = '
            {
                "features": [{"name": "foo", "value": 1},{"name": "bar", "value": 2},{"name": "baz", "value": 1}]
            }
        ';

        return json_decode($json, $asArray);
    }


}

class JSONPathTestClass
{
    protected $attributes = [
        'foo' => 'bar',
    ];

    public function __get($key)
    {
        return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
    }
}