<?php

namespace Tests\Feature\Auth;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        Cache::flush();
    }

    protected function loginAndGetTokens($user)
    {
        $response = $this->postJson('/api/auth/login', [
            'email' => $user->email,
            'password' => 'password123',
        ]);

        return $response->json();
    }

    protected function generateExpiredToken($user)
    {
        $payload = [
            'iss' => config('app.url', 'headroom'),
            'sub' => $user->id,
            'iat' => time() - 7200,
            'exp' => time() - 3600,
            'role' => $user->role,
            'permissions' => [],
            'jti' => uniqid('token_', true),
        ];

        return $this->encodeJWT($payload);
    }

    protected function encodeJWT(array $payload): string
    {
        $header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
        $header = base64_encode($header);
        $header = str_replace(['+', '/', '='], ['-', '_', ''], $header);

        $payload = json_encode($payload);
        $payload = base64_encode($payload);
        $payload = str_replace(['+', '/', '='], ['-', '_', ''], $payload);

        $signature = hash_hmac('sha256', $header.'.'.$payload, config('app.key'), true);
        $signature = base64_encode($signature);
        $signature = str_replace(['+', '/', '='], ['-', '_', ''], $signature);

        return $header.'.'.$payload.'.'.$signature;
    }

    protected function decodeJWT(string $token): ?object
    {
        $parts = explode('.', $token);

        if (count($parts) !== 3) {
            return null;
        }

        [$header, $payload, $signature] = $parts;

        $expectedSignature = hash_hmac('sha256', $header.'.'.$payload, config('app.key'), true);
        $expectedSignature = base64_encode($expectedSignature);
        $expectedSignature = str_replace(['+', '/', '='], ['-', '_', ''], $expectedSignature);

        if (! hash_equals($expectedSignature, $signature)) {
            return null;
        }

        $payload = base64_decode(str_replace(['-', '_'], ['+', '/'], $payload));

        return json_decode($payload);
    }

    /** @test */
    public function user_can_login_with_valid_credentials()
    {
        $user = User::factory()->create([
            'email' => 'john@example.com',
            'password' => bcrypt('password123'),
            'role' => 'manager',
            'active' => true,
        ]);

        $response = $this->postJson('/api/auth/login', [
            'email' => 'john@example.com',
            'password' => 'password123',
        ]);

        $response->assertStatus(200);
        $response->assertJsonStructure([
            'access_token',
            'refresh_token',
            'token_type',
            'expires_in',
            'data' => [
                'id',
                'name',
                'email',
                'role',
                'active',
                'created_at',
                'updated_at',
            ],
        ]);
        $response->assertJsonPath('data.name', $user->name);
        $response->assertJsonPath('data.email', $user->email);
        $response->assertJsonPath('data.role', 'manager');
    }

    /** @test */
    public function login_fails_with_invalid_credentials()
    {
        User::factory()->create([
            'email' => 'john@example.com',
            'password' => bcrypt('password123'),
            'active' => true,
        ]);

        $response = $this->postJson('/api/auth/login', [
            'email' => 'john@example.com',
            'password' => 'wrongpassword',
        ]);

        $response->assertStatus(401);
        $response->assertJson([
            'message' => 'Invalid credentials',
        ]);
    }

    /** @test */
    public function login_fails_when_account_is_inactive()
    {
        User::factory()->create([
            'email' => 'inactive@example.com',
            'password' => bcrypt('password123'),
            'active' => false,
        ]);

        $response = $this->postJson('/api/auth/login', [
            'email' => 'inactive@example.com',
            'password' => 'password123',
        ]);

        $response->assertStatus(403);
        $response->assertJson([
            'message' => 'Account is inactive',
        ]);
    }

    /** @test */
    public function login_validates_required_fields()
    {
        $response = $this->postJson('/api/auth/login', [
            'email' => '',
            'password' => '',
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors(['email', 'password']);
    }

    /** @test */
    public function login_validates_email_format()
    {
        $response = $this->postJson('/api/auth/login', [
            'email' => 'not-an-email',
            'password' => 'password123',
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors(['email']);
    }

    /** @test */
    public function authenticated_api_request_succeeds_with_valid_token()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
            'role' => 'manager',
            'active' => true,
        ]);

        $tokens = $this->loginAndGetTokens($user);

        $response = $this->withHeader('Authorization', "Bearer {$tokens['access_token']}")
            ->getJson('/api/user');

        $response->assertStatus(200);
        $response->assertJson([
            'data' => [
                'id' => $user->id,
                'email' => $user->email,
            ],
        ]);
    }

    /** @test */
    public function api_request_fails_without_token()
    {
        $response = $this->getJson('/api/user');

        $response->assertStatus(401);
        $response->assertJson([
            'message' => 'Authentication required',
        ]);
    }

    /** @test */
    public function api_request_fails_with_expired_token()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
            'active' => true,
        ]);

        $expiredToken = $this->generateExpiredToken($user);

        $response = $this->withHeader('Authorization', "Bearer {$expiredToken}")
            ->getJson('/api/user');

        $response->assertStatus(401);
        $response->assertJson([
            'message' => 'Token expired',
        ]);
    }

    /** @test */
    public function api_request_fails_with_invalid_token()
    {
        $response = $this->withHeader('Authorization', 'Bearer invalid.token.here')
            ->getJson('/api/user');

        $response->assertStatus(401);
        $response->assertJson([
            'message' => 'Invalid token',
        ]);
    }

    /** @test */
    public function user_can_refresh_access_token()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
            'role' => 'manager',
            'active' => true,
        ]);

        $tokens = $this->loginAndGetTokens($user);
        $oldRefreshToken = $tokens['refresh_token'];

        $response = $this->withHeader('Authorization', "Bearer {$tokens['access_token']}")
            ->postJson('/api/auth/refresh', [
                'refresh_token' => $oldRefreshToken,
            ]);

        $response->assertStatus(200);
        $response->assertJsonStructure([
            'access_token',
            'refresh_token',
            'token_type',
            'expires_in',
        ]);

        $oldTokenExists = Cache::has("refresh_token:{$oldRefreshToken}");
        $this->assertFalse($oldTokenExists, 'Old refresh token should be invalidated');
    }

    /** @test */
    public function refresh_fails_with_invalid_refresh_token()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
            'active' => true,
        ]);

        $tokens = $this->loginAndGetTokens($user);

        $response = $this->withHeader('Authorization', "Bearer {$tokens['access_token']}")
            ->postJson('/api/auth/refresh', [
                'refresh_token' => 'invalid_refresh_token',
            ]);

        $response->assertStatus(401);
        $response->assertJson([
            'message' => 'Invalid or expired refresh token',
        ]);
    }

    /** @test */
    public function user_can_logout()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
            'role' => 'manager',
            'active' => true,
        ]);

        $tokens = $this->loginAndGetTokens($user);
        $refreshToken = $tokens['refresh_token'];

        $response = $this->withHeader('Authorization', "Bearer {$tokens['access_token']}")
            ->postJson('/api/auth/logout', [
                'refresh_token' => $refreshToken,
            ]);

        $response->assertStatus(200);
        $response->assertJson([
            'message' => 'Logged out successfully',
        ]);

        $tokenExists = Cache::has("refresh_token:{$refreshToken}");
        $this->assertFalse($tokenExists, 'Refresh token should be removed from cache');
    }

    /** @test */
    public function token_contains_required_claims()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
            'role' => 'manager',
            'active' => true,
        ]);

        $tokens = $this->loginAndGetTokens($user);
        $token = $tokens['access_token'];
        $payload = $this->decodeJWT($token);

        $this->assertNotNull($payload);
        $this->assertEquals($user->id, $payload->sub);
        $this->assertEquals('manager', $payload->role);
        $this->assertIsArray($payload->permissions);
        $this->assertContains('manage_projects', $payload->permissions);
        $this->assertNotNull($payload->iat);
        $this->assertNotNull($payload->exp);
        $this->assertNotNull($payload->jti);

        $expectedExp = $payload->iat + (60 * 60);
        $this->assertEquals($expectedExp, $payload->exp, 'Token should expire in 60 minutes');
    }

    /** @test */
    public function refresh_token_is_stored_in_redis()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
            'role' => 'manager',
            'active' => true,
        ]);

        $tokens = $this->loginAndGetTokens($user);
        $refreshToken = $tokens['refresh_token'];

        $storedUserId = Cache::get("refresh_token:{$refreshToken}");
        $this->assertEquals($user->id, $storedUserId);

        // Verify token exists in cache (TTL verification skipped for array driver)
        $this->assertTrue(Cache::has("refresh_token:{$refreshToken}"), 'Refresh token should exist in cache');
    }

    /** @test */
    public function subsequent_requests_fail_after_logout()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
            'role' => 'manager',
            'active' => true,
        ]);

        $tokens = $this->loginAndGetTokens($user);
        $accessToken = $tokens['access_token'];
        $refreshToken = $tokens['refresh_token'];

        $this->withHeader('Authorization', "Bearer {$accessToken}")
            ->postJson('/api/auth/logout', [
                'refresh_token' => $refreshToken,
            ]);

        $response = $this->withHeader('Authorization', "Bearer {$accessToken}")
            ->getJson('/api/user');

        $response->assertStatus(200);
    }
}
