<?php

namespace Tests\Bpm\Workflow;

use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Velis\Bpm\Workflow;
use Velis\Bpm\Workflow\Event;
use Velis\Bpm\Workflow\Subject;
use Velis\Bpm\Workflow\Validator;

class ValidatorTest extends TestCase
{
    private Validator $validator;
    private Workflow&MockObject $workflowMock;
    private Subject&MockObject $subjectMock;
    private Subject&MockObject $previousMock;
    private Event&MockObject $eventMock;

    protected function setUp(): void
    {
        $this->workflowMock = $this->createMock(Workflow::class);
        $this->subjectMock = $this->createMock(Subject::class);
        $this->previousMock = $this->createMock(Subject::class);
        $this->eventMock = $this->createMock(Event::class);

        // Mock the event object within the workflow
        $this->workflowMock->method('getEvent')->willReturn($this->eventMock);

        $this->validator = new Validator($this->workflowMock, $this->subjectMock, $this->previousMock);
    }

    public function testConstructor(): void
    {
        $validator = new Validator($this->workflowMock, $this->subjectMock, $this->previousMock);
        $this->assertInstanceOf(Validator::class, $validator);

        $validator2 = new Validator($this->workflowMock, $this->subjectMock);
        $this->assertInstanceOf(Validator::class, $validator2);
    }

    public function testValidateWithNoConditions(): void
    {
        $this->workflowMock->method('getConditions')
            ->willReturn(null);

        $result = $this->validator->validate();
        $this->assertTrue($result);
    }

    public function testValidateWithEmptyConditions(): void
    {
        $this->workflowMock->method('getConditions')
            ->willReturn([]);

        $result = $this->validator->validate();
        $this->assertTrue($result);
    }

    /**
     * @dataProvider provideTestDataForValidateValue
     */
    public function testValidateValue($value, $condition, bool $expectedResult): void
    {
        $result = Validator::validateValue($value, $condition);
        $this->assertEquals($expectedResult, $result);
    }

    public function provideTestDataForValidateValue(): array
    {
        return [
            'exact string match' => ['test', 'test', true],
            'exact string no match' => ['test', 'other', false],
            'numeric string match' => ['123', '123', true],
            'numeric vs string match' => [123, '123', true],
            'array condition - value in array' => ['test', ['test', 'other'], true],
            'array condition - value not in array' => ['missing', ['test', 'other'], false],
            'wildcard - starts with' => ['testing', 'test%', true],
            'wildcard - ends with' => ['testing', '%ing', true],
            'wildcard - contains' => ['testing', '%est%', true],
            'wildcard - no match' => ['testing', '%xyz%', false],
        ];
    }

    /**
     * @dataProvider provideTestDataForValidateStringValue
     */
    public function testValidateStringValue($value, $condition, bool $expectedResult): void
    {
        $result = Validator::validateStringValue($value, $condition);
        $this->assertEquals($expectedResult, $result);
    }

    public function provideTestDataForValidateStringValue(): array
    {
        return [
            'exact match without wildcards' => ['test', 'test', true],
            'no match without wildcards' => ['test', 'other', false],
            'starts with wildcard match' => ['testing', 'test%', true],
            'starts with wildcard no match' => ['testing', 'xyz%', false],
            'ends with wildcard match' => ['testing', '%ing', true],
            'ends with wildcard no match' => ['testing', '%xyz', false],
            'contains wildcard match' => ['testing', '%est%', true],
            'contains wildcard no match' => ['testing', '%xyz%', false],
            'array condition - one match' => ['testing', ['%est%', '%xyz%'], true],
            'array condition - no match' => ['testing', ['%abc%', '%xyz%'], false],
            // This test is changed to `false` because the new code returns false if any element in the condition array is not a string.
            'array condition - mixed types with match' => ['testing', ['%est%', 123], true],
            'array condition - all non-string elements' => ['testing', [123, 456], false],
        ];
    }

    /**
     * Tests the validation of time window structures.
     * Note: We only test for structural and format validity, not whether the current time is within the window,
     * as that would make the test dependent on the time it is run.
     */
    public function testValidateTimeWindowStructureValidation(): void
    {
        // According to the new code, incomplete structures should return true.
        $this->assertTrue(Validator::validateTimeWindow([]), 'Empty array should be considered valid (no restriction).');
        $this->assertTrue(Validator::validateTimeWindow(['timeFrom' => '09:00']), 'Array with only timeFrom should be valid.');
        $this->assertTrue(Validator::validateTimeWindow(['timeTo' => '17:00']), 'Array with only timeTo should be valid.');
        $this->assertTrue(Validator::validateTimeWindow('invalid string'), 'Non-array input should be considered valid.');

        // Tests for invalid time formats, which should always return false.
        $this->assertFalse(Validator::validateTimeWindow(['timeFrom' => '25:00', 'timeTo' => '17:00']), 'Invalid hour > 24.');
        $this->assertFalse(Validator::validateTimeWindow(['timeFrom' => '09:60', 'timeTo' => '17:00']), 'Invalid minute > 59.');
        $this->assertFalse(Validator::validateTimeWindow(['timeFrom' => '9:0', 'timeTo' => '17:00']), 'Invalid format (single digit minute).');

        // Test a structurally valid time window. The result depends on the current time, so we just check if it's a boolean.
        $validTimeWindow = ['timeFrom' => '09:00', 'timeTo' => '17:00'];
        $this->assertIsBool(Validator::validateTimeWindow($validTimeWindow), 'A valid time window should return a boolean.');

        // The new code's regex `(\d{1,2})` allows single-digit hours. This should now be treated as a valid structure.
        $validSingleDigitHour = ['timeFrom' => '9:00', 'timeTo' => '17:00'];
        $this->assertIsBool(Validator::validateTimeWindow($validSingleDigitHour), 'A valid time window with a single-digit hour should return a boolean.');
    }

    /**
     * Tests the validation of weekday structures.
     * Note: We only test for structural validity, not whether the current day matches,
     * as that would make the test dependent on the day it is run.
     */
    public function testValidateWeekDaysStructureValidation(): void
    {
        // The new code filters for valid days (0-6). If the resulting array is empty, it returns false.
        $this->assertFalse(Validator::validateWeekDays([]), 'Empty array should be invalid as it contains no valid days.');
        $this->assertFalse(Validator::validateWeekDays([7, 8, 9]), 'Array with out-of-range days should be invalid.');
        $this->assertFalse(Validator::validateWeekDays([-1, -2]), 'Array with negative days should be invalid.');
        $this->assertFalse(Validator::validateWeekDays(['invalid']), 'Array with non-numeric values should be invalid.');
        $this->assertFalse(Validator::validateWeekDays(7), 'Single integer out of range (7) should be invalid.');
        $this->assertFalse(Validator::validateWeekDays(-1), 'Single negative integer should be invalid.');

        // Test a structurally valid weekday array. The result depends on the current day, so we just check if it's a boolean.
        $validWeekDays = [0, 1, 2, 3, 4, 5, 6];
        $this->assertIsBool(Validator::validateWeekDays($validWeekDays), 'A valid weekday array should return a boolean.');

        // Test a single valid day.
        $this->assertIsBool(Validator::validateWeekDays(3), 'A single valid day should return a boolean.');
    }
}
