<?php

declare(strict_types=1);

namespace Tests\Feature\Masters;

use App\Enums\AuditEvent;
use App\Models\AuditLog;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class TeamTest extends TestCase
{
    use RefreshDatabase;

    // -------------------------------------------------------------- access

    #[Test]
    public function a_guest_cannot_reach_teams(): void
    {
        $this->get(route('masters.teams.index'))->assertRedirect(route('login'));
    }

    #[Test]
    public function any_signed_in_user_can_manage_teams(): void
    {
        // Master data is not master-only: everyday work is open to both user
        // types. Only deletion and restoration are restricted.
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('masters.teams.index'))->assertOk();
        $this->actingAs($user)->get(route('masters.teams.create'))->assertOk();

        $this->actingAs($user)
            ->post(route('masters.teams.store'), [
                'name' => 'Engineering',
                'code' => 'ENG',
                'is_active' => '1',
            ])
            ->assertRedirect(route('masters.teams.index'));

        $this->assertDatabaseHas('teams', ['name' => 'Engineering']);
    }

    #[Test]
    public function a_regular_user_cannot_delete_or_restore(): void
    {
        $user = User::factory()->create();
        $team = Team::factory()->create();

        $this->actingAs($user)
            ->delete(route('masters.teams.destroy', $team))
            ->assertForbidden();

        $team->delete();

        $this->actingAs($user)
            ->patch(route('masters.teams.restore', $team))
            ->assertForbidden();

        $this->assertSoftDeleted('teams', ['id' => $team->getKey()]);
    }

    // ------------------------------------------------------------- listing

    #[Test]
    public function the_listing_can_be_searched(): void
    {
        Team::factory()->create(['name' => 'Engineering', 'code' => 'ENG']);
        Team::factory()->create(['name' => 'Human Resources', 'code' => 'HR']);

        $this->actingAs(User::factory()->create())
            ->get(route('masters.teams.index', ['search' => 'engin']))
            ->assertOk()
            ->assertSee('Engineering')
            ->assertDontSee('Human Resources');
    }

    #[Test]
    public function the_listing_can_be_searched_by_code(): void
    {
        Team::factory()->create(['name' => 'Engineering', 'code' => 'ENG']);
        Team::factory()->create(['name' => 'Human Resources', 'code' => 'HR']);

        $this->actingAs(User::factory()->create())
            ->get(route('masters.teams.index', ['search' => 'HR']))
            ->assertOk()
            ->assertSee('Human Resources')
            ->assertDontSee('Engineering');
    }

    #[Test]
    public function the_listing_can_be_filtered_by_status(): void
    {
        Team::factory()->create(['name' => 'Engineering']);
        Team::factory()->inactive()->create(['name' => 'Legacy Support']);

        $this->actingAs(User::factory()->create())
            ->get(route('masters.teams.index', ['status' => 'inactive']))
            ->assertOk()
            ->assertSee('Legacy Support')
            ->assertDontSee('Engineering');
    }

    #[Test]
    public function the_listing_is_paginated(): void
    {
        Team::factory()->count(30)->create();

        $response = $this->actingAs(User::factory()->create())
            ->get(route('masters.teams.index'));

        $response->assertOk();
        $this->assertSame(25, $response->viewData('teams')->perPage());
        $this->assertSame(30, $response->viewData('teams')->total());
        $this->assertCount(25, $response->viewData('teams')->items());
    }

    #[Test]
    public function deleted_teams_are_hidden_from_the_normal_listing(): void
    {
        $team = Team::factory()->create(['name' => 'Engineering']);
        $team->delete();

        $this->actingAs(User::factory()->create())
            ->get(route('masters.teams.index'))
            ->assertOk()
            ->assertDontSee('Engineering');
    }

    // -------------------------------------------------------------- create

    #[Test]
    public function the_name_is_required(): void
    {
        $this->actingAs(User::factory()->create())
            ->post(route('masters.teams.store'), ['name' => '', 'is_active' => '1'])
            ->assertSessionHasErrors('name');
    }

    #[Test]
    public function the_name_must_be_unique(): void
    {
        Team::factory()->create(['name' => 'Engineering']);

        $this->actingAs(User::factory()->create())
            ->post(route('masters.teams.store'), ['name' => 'Engineering', 'is_active' => '1'])
            ->assertSessionHasErrors('name');
    }

    #[Test]
    public function a_deleted_name_still_blocks_a_duplicate(): void
    {
        /*
         | The unique index covers soft-deleted rows, so validating only live
         | ones would let this through and then fail at insert with a database
         | error. The message points at the deleted list instead.
         */
        $team = Team::factory()->create(['name' => 'Engineering']);
        $team->delete();

        $this->actingAs(User::factory()->create())
            ->post(route('masters.teams.store'), ['name' => 'Engineering', 'is_active' => '1'])
            ->assertSessionHasErrors('name');
    }

    #[Test]
    public function the_code_is_optional_and_upper_cased(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)->post(route('masters.teams.store'), [
            'name' => 'Engineering',
            'code' => 'eng',
            'is_active' => '1',
        ])->assertSessionHasNoErrors();

        $this->assertSame('ENG', Team::query()->where('name', 'Engineering')->sole()->code);

        $this->actingAs($user)->post(route('masters.teams.store'), [
            'name' => 'Finance',
            'code' => '',
            'is_active' => '1',
        ])->assertSessionHasNoErrors();

        $this->assertNull(Team::query()->where('name', 'Finance')->sole()->code);
    }

    #[Test]
    public function the_code_must_be_unique_and_well_formed(): void
    {
        Team::factory()->create(['code' => 'ENG']);
        $user = User::factory()->create();

        $this->actingAs($user)
            ->post(route('masters.teams.store'), ['name' => 'Other', 'code' => 'ENG', 'is_active' => '1'])
            ->assertSessionHasErrors('code');

        $this->actingAs($user)
            ->post(route('masters.teams.store'), ['name' => 'Other', 'code' => 'E N G!', 'is_active' => '1'])
            ->assertSessionHasErrors('code');
    }

    #[Test]
    public function the_name_is_trimmed(): void
    {
        $this->actingAs(User::factory()->create())->post(route('masters.teams.store'), [
            'name' => '   Engineering   ',
            'is_active' => '1',
        ]);

        $this->assertDatabaseHas('teams', ['name' => 'Engineering']);
    }

    #[Test]
    public function creating_a_team_is_audited(): void
    {
        $user = User::factory()->create();
        AuditLog::query()->delete();

        $this->actingAs($user)->post(route('masters.teams.store'), [
            'name' => 'Engineering',
            'code' => 'ENG',
            'is_active' => '1',
        ]);

        $log = AuditLog::query()->sole();

        $this->assertSame(AuditEvent::Created, $log->event);
        $this->assertSame('Created Team Engineering', $log->description);
        $this->assertSame($user->getKey(), $log->user_id);
    }

    // -------------------------------------------------------------- update

    #[Test]
    public function a_team_can_be_updated(): void
    {
        $team = Team::factory()->create(['name' => 'Engineering', 'code' => 'ENG']);

        $this->actingAs(User::factory()->create())
            ->put(route('masters.teams.update', $team), [
                'name' => 'Product Engineering',
                'code' => 'PENG',
                'is_active' => '0',
            ])
            ->assertRedirect(route('masters.teams.index'));

        $team->refresh();

        $this->assertSame('Product Engineering', $team->name);
        $this->assertSame('PENG', $team->code);
        $this->assertFalse($team->is_active);
    }

    #[Test]
    public function keeping_its_own_name_is_not_a_conflict(): void
    {
        $team = Team::factory()->create(['name' => 'Engineering', 'code' => 'ENG']);

        $this->actingAs(User::factory()->create())
            ->put(route('masters.teams.update', $team), [
                'name' => 'Engineering',
                'code' => 'ENG',
                'is_active' => '1',
            ])
            ->assertSessionHasNoErrors();
    }

    #[Test]
    public function another_teams_name_is_a_conflict(): void
    {
        Team::factory()->create(['name' => 'Engineering']);
        $other = Team::factory()->create(['name' => 'Finance']);

        $this->actingAs(User::factory()->create())
            ->put(route('masters.teams.update', $other), [
                'name' => 'Engineering',
                'is_active' => '1',
            ])
            ->assertSessionHasErrors('name');
    }

    #[Test]
    public function updating_a_team_is_audited_with_its_diff(): void
    {
        $user = User::factory()->create();
        $team = Team::factory()->create(['name' => 'Engineering', 'code' => 'ENG']);
        AuditLog::query()->delete();

        $this->actingAs($user)->put(route('masters.teams.update', $team), [
            'name' => 'Product Engineering',
            'code' => 'ENG',
            'is_active' => '1',
        ]);

        $log = AuditLog::query()->sole();

        $this->assertSame(AuditEvent::Updated, $log->event);
        $this->assertSame(['name' => 'Engineering'], $log->old_values);
        $this->assertSame(['name' => 'Product Engineering'], $log->new_values);
    }

    // ------------------------------------------------------ delete/restore

    #[Test]
    public function a_master_can_soft_delete_a_team(): void
    {
        $team = Team::factory()->create(['name' => 'Engineering']);

        $this->actingAs(User::factory()->master()->create())
            ->delete(route('masters.teams.destroy', $team))
            ->assertRedirect(route('masters.teams.index'))
            ->assertSessionHas('success');

        $this->assertSoftDeleted('teams', ['id' => $team->getKey()]);
    }

    #[Test]
    public function deleting_a_team_is_audited(): void
    {
        $master = User::factory()->master()->create();
        $team = Team::factory()->create(['name' => 'Engineering']);
        AuditLog::query()->delete();

        $this->actingAs($master)
            ->delete(route('masters.teams.destroy', $team));

        $log = AuditLog::query()->sole();

        $this->assertSame(AuditEvent::Deleted, $log->event);
        $this->assertSame('Deleted Team Engineering', $log->description);
    }

    #[Test]
    public function deleted_teams_appear_in_the_deleted_listing(): void
    {
        $team = Team::factory()->create(['name' => 'Engineering']);
        $team->delete();

        $response = $this->actingAs(User::factory()->master()->create())
            ->get(route('masters.teams.index', ['trashed' => 1]));

        $response->assertOk();
        $response->assertSee('Engineering');
        $response->assertSee('Deleted Teams');
        $this->assertTrue($response->viewData('showingDeleted'));
    }

    #[Test]
    public function a_master_can_restore_a_team(): void
    {
        $team = Team::factory()->create(['name' => 'Engineering']);
        $team->delete();

        $this->actingAs(User::factory()->master()->create())
            ->patch(route('masters.teams.restore', $team))
            ->assertRedirect(route('masters.teams.index'))
            ->assertSessionHas('success');

        $this->assertNotSoftDeleted('teams', ['id' => $team->getKey()]);
    }

    #[Test]
    public function restoring_a_team_is_audited(): void
    {
        $master = User::factory()->master()->create();
        $team = Team::factory()->create(['name' => 'Engineering']);
        $team->delete();
        AuditLog::query()->delete();

        $this->actingAs($master)
            ->patch(route('masters.teams.restore', $team));

        // One entry, not also an "updated" for deleted_at going null.
        $log = AuditLog::query()->sole();

        $this->assertSame(AuditEvent::Restored, $log->event);
        $this->assertSame('Restored Team Engineering', $log->description);
    }

    #[Test]
    public function a_restored_team_can_be_edited_again(): void
    {
        $team = Team::factory()->create(['name' => 'Engineering']);
        $team->delete();

        $master = User::factory()->master()->create();
        $this->actingAs($master)->patch(route('masters.teams.restore', $team));

        $this->actingAs($master)
            ->get(route('masters.teams.edit', $team->fresh()))
            ->assertOk();
    }

    #[Test]
    public function the_deleted_listing_is_only_offered_to_masters(): void
    {
        Team::factory()->create()->delete();

        // The count now sits in a badge beside the label rather than in the
        // label text, so the assertion is on the link itself — which is the
        // thing this test is actually about.
        $trashedUrl = route('masters.teams.index', ['trashed' => 1]);

        $this->actingAs(User::factory()->create())
            ->get(route('masters.teams.index'))
            ->assertOk()
            ->assertDontSee($trashedUrl, escape: false);

        $this->actingAs(User::factory()->master()->create())
            ->get(route('masters.teams.index'))
            ->assertOk()
            ->assertSee($trashedUrl, escape: false);
    }
}
