<?php

declare(strict_types=1);

namespace App\Tests\Unit\Ip;

use App\Domain\Ip\Cidr;
use App\Domain\Ip\InvalidCidrException;
use App\Domain\Ip\IpAddress;
use PHPUnit\Framework\TestCase;

final class CidrTest extends TestCase
{
    public function testParsesSimpleIpv4Cidr(): void
    {
        $cidr = Cidr::fromString('10.0.0.0/8');

        $this->assertTrue($cidr->isIpv4());
        $this->assertSame(8, $cidr->originalPrefix());
        $this->assertSame(104, $cidr->prefixLength());
        $this->assertSame('10.0.0.0/8', $cidr->text());
    }

    public function testParsesSlash32SingleHost(): void
    {
        $cidr = Cidr::fromString('192.168.1.42/32');

        $this->assertTrue($cidr->contains(IpAddress::fromString('192.168.1.42')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('192.168.1.43')));
    }

    public function testV4Slash24Containment(): void
    {
        $cidr = Cidr::fromString('10.0.0.0/24');

        $this->assertTrue($cidr->contains(IpAddress::fromString('10.0.0.0')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('10.0.0.1')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('10.0.0.255')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('10.0.1.0')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('11.0.0.0')));
    }

    public function testV4Slash16Containment(): void
    {
        $cidr = Cidr::fromString('172.16.0.0/16');

        $this->assertTrue($cidr->contains(IpAddress::fromString('172.16.0.0')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('172.16.255.255')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('172.17.0.0')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('172.15.255.255')));
    }

    public function testV4Slash0MatchesAllV4(): void
    {
        $cidr = Cidr::fromString('0.0.0.0/0');

        $this->assertSame(96, $cidr->prefixLength());
        $this->assertTrue($cidr->contains(IpAddress::fromString('0.0.0.0')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('255.255.255.255')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('1.2.3.4')));

        // /0 of v4 = ::ffff:0:0/96 → does NOT match pure IPv6
        $this->assertFalse($cidr->contains(IpAddress::fromString('2001:db8::1')));
    }

    public function testV6Slash0MatchesEverything(): void
    {
        $cidr = Cidr::fromString('::/0');

        $this->assertSame(0, $cidr->prefixLength());
        $this->assertTrue($cidr->contains(IpAddress::fromString('::1')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('2001:db8::1')));
        // Even v4 (which is stored as v4-mapped v6) matches /0
        $this->assertTrue($cidr->contains(IpAddress::fromString('1.2.3.4')));
    }

    public function testV6Slash128SingleHost(): void
    {
        $cidr = Cidr::fromString('2001:db8::1/128');

        $this->assertTrue($cidr->contains(IpAddress::fromString('2001:db8::1')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('2001:db8::2')));
    }

    public function testV6Slash64Containment(): void
    {
        $cidr = Cidr::fromString('2001:db8::/64');

        $this->assertTrue($cidr->contains(IpAddress::fromString('2001:db8::1')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('2001:db8::ffff:ffff:ffff:ffff')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('2001:db9::1')));
    }

    public function testV6CidrDoesNotContainV4(): void
    {
        $cidr = Cidr::fromString('2001:db8::/32');

        $this->assertFalse($cidr->contains(IpAddress::fromString('1.2.3.4')));
    }

    public function testV4InV4MappedV6Cidr(): void
    {
        // ::ffff:1.0.0.0/120 covers v4-mapped 1.0.0.0/24
        $cidr = Cidr::fromString('::ffff:1.0.0.0/120');

        $this->assertFalse($cidr->isIpv4(), 'CIDR is in v6 syntax even though it covers v4');
        $this->assertSame(120, $cidr->prefixLength());

        $this->assertTrue($cidr->contains(IpAddress::fromString('1.0.0.5')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('1.0.0.255')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('1.0.1.0')));
    }

    public function testV4MappedV6AddressInsideV4Cidr(): void
    {
        $cidr = Cidr::fromString('1.0.0.0/24');

        // The v4-mapped IPv6 form must still be contained because internal storage is unified.
        $this->assertTrue($cidr->contains(IpAddress::fromString('::ffff:1.0.0.7')));
    }

    public function testNetworkAddressMaskedFromHostBits(): void
    {
        $cidr = Cidr::fromString('10.1.2.99/24');
        $expected = IpAddress::fromString('10.1.2.0');

        $this->assertSame($expected->binary(), $cidr->network());
        $this->assertSame('10.1.2.0/24', $cidr->text());
    }

    public function testV6NetworkAddressMaskedFromHostBits(): void
    {
        $cidr = Cidr::fromString('2001:db8:abcd:1234::ff/56');

        $this->assertSame('2001:db8:abcd:1200::/56', $cidr->text());
    }

    public function testRejectsMissingSlash(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('10.0.0.0');
    }

    public function testRejectsEmpty(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('');
    }

    public function testRejectsBadIp(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('garbage/8');
    }

    public function testRejectsNegativePrefix(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('10.0.0.0/-1');
    }

    public function testRejectsV4PrefixOver32(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('10.0.0.0/33');
    }

    public function testRejectsV6PrefixOver128(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('2001:db8::/129');
    }

    public function testRejectsNonNumericPrefix(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('10.0.0.0/abc');
    }

    public function testRejectsMultipleSlashes(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('10.0.0.0/24/8');
    }

    public function testRejectsLeadingWhitespace(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString(' 10.0.0.0/24');
    }

    public function testRejectsEmptyPrefix(): void
    {
        $this->expectException(InvalidCidrException::class);
        Cidr::fromString('10.0.0.0/');
    }

    public function testNonByteAlignedV4Prefix(): void
    {
        // /20 fixes the first 20 bits = 8 (byte 0) + 8 (byte 1) + 4 (high
        // nibble of byte 2). Exercises the partial-byte mask path.
        // 10.16.0.0/20 covers 10.16.0.0 — 10.16.15.255 (4096 addresses).
        $cidr = Cidr::fromString('10.16.0.0/20');

        $this->assertTrue($cidr->contains(IpAddress::fromString('10.16.0.0')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('10.16.15.255')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('10.16.5.42')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('10.16.16.0')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('10.15.255.255')));
        $this->assertSame('10.16.0.0/20', $cidr->text());
    }

    public function testNonByteAlignedV6Prefix(): void
    {
        // /17 fixes 16 bits + 1 high bit of byte 2. 2001:8000::/17 covers
        // any address whose first 17 bits start with 0x2001 followed by a 1.
        $cidr = Cidr::fromString('2001:8000::/17');

        $this->assertTrue($cidr->contains(IpAddress::fromString('2001:8000::')));
        $this->assertTrue($cidr->contains(IpAddress::fromString('2001:ffff:ffff:ffff:ffff:ffff:ffff:ffff')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('2001:7fff:ffff::')));
        $this->assertFalse($cidr->contains(IpAddress::fromString('2002::')));
    }

    public function testNonByteAlignedHostMaskingClearsBits(): void
    {
        // 10.16.5.7/20 should mask the host bits to 10.16.0.0/20
        $cidr = Cidr::fromString('10.16.5.7/20');

        $this->assertSame('10.16.0.0/20', $cidr->text());
    }
}
