<?php

namespace Tests\Feature\Communication;

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Communication\Enums\TicketStatus;
use App\Domain\Communication\Models\NotificationMessage;
use App\Domain\Identity\Models\User;
use App\Domain\Organization\Models\Branch;
use App\Domain\Organization\Models\Company;
use App\Domain\Support\Models\SupportTicket;
use App\Domain\Support\Models\SupportTicketMessage;
use Database\Seeders\NotificationTemplateSeeder;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use RuntimeException;
use Tests\TestCase;

class SupportTicketTest extends TestCase
{
    use RefreshDatabase;

    private Company $company;

    private Branch $branch;

    private Applicant $applicant;

    private User $portalUser;

    protected function setUp(): void
    {
        parent::setUp();
        Mail::fake();
        $this->seed([PermissionSeeder::class, RoleSeeder::class]);

        $this->company = Company::factory()->create();
        $this->branch = Branch::factory()->for($this->company)->create();
        $this->seed(NotificationTemplateSeeder::class);

        $this->applicant = Applicant::factory()->for($this->company)->create([
            'branch_id' => $this->branch->id,
            'email' => 'applicant@example.test',
        ]);

        $this->portalUser = tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
            'user_type' => 'applicant',
            'email' => 'applicant@example.test',
        ]))->assignRole('applicant');

        $this->applicant->update(['user_id' => $this->portalUser->id]);
    }

    private function user(string $role): User
    {
        return tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
        ]))->assignRole($role);
    }

    private function openTicket(array $overrides = []): string
    {
        return $this->actingAs($this->portalUser)
            ->postJson('/api/v1/tickets', array_merge([
                'subject' => 'My payment is not showing',
                'category' => 'payment',
                'body' => 'I paid last week and the portal still says I owe money.',
            ], $overrides))
            ->assertCreated()
            ->json('data.id');
    }

    public function test_an_applicant_can_raise_a_ticket_about_their_own_file(): void
    {
        $id = $this->openTicket();

        $ticket = SupportTicket::where('uuid', $id)->firstOrFail();

        $this->assertSame($this->applicant->id, $ticket->applicant_id);
        $this->assertSame(TicketStatus::Open, $ticket->status);
        $this->assertSame('applicant', $ticket->messages()->first()->author_side);
    }

    public function test_the_sla_is_set_from_the_category_not_the_form(): void
    {
        // A payment complaint is answered inside a day.
        $payment = SupportTicket::where('uuid', $this->openTicket())->firstOrFail();

        $service = SupportTicket::where('uuid', $this->openTicket([
            'category' => 'service',
            'subject' => 'A question about training',
            'body' => 'When does the orientation start?',
        ]))->firstOrFail();

        $this->assertSame(24, (int) round($payment->opened_at->diffInHours($payment->due_at)));
        $this->assertSame(72, (int) round($service->opened_at->diffInHours($service->due_at)));
    }

    public function test_opening_a_ticket_confirms_receipt_to_the_applicant(): void
    {
        $this->openTicket();

        $this->assertGreaterThan(
            0,
            NotificationMessage::where('template_code', 'ticket_opened')->count(),
        );
    }

    public function test_a_staff_reply_stops_the_sla_clock(): void
    {
        $id = $this->openTicket();

        $this->actingAs($this->user('support-officer'))
            ->postJson("/api/v1/tickets/{$id}/reply", ['body' => 'We are checking with accounts now.'])
            ->assertOk();

        $ticket = SupportTicket::where('uuid', $id)->firstOrFail();

        $this->assertNotNull($ticket->first_responded_at);
        $this->assertSame(TicketStatus::WaitingApplicant, $ticket->status);
        $this->assertFalse($ticket->isOverdue());
    }

    public function test_an_internal_note_does_not_stop_the_clock(): void
    {
        $id = $this->openTicket();

        $this->actingAs($this->user('support-officer'))
            ->postJson("/api/v1/tickets/{$id}/reply", [
                'body' => 'Checked the bank feed, nothing there yet.',
                'is_internal' => true,
            ])
            ->assertOk();

        $ticket = SupportTicket::where('uuid', $id)->firstOrFail();

        // The agency talking to itself is not an answer to the applicant.
        $this->assertNull($ticket->first_responded_at);
        $this->assertSame(TicketStatus::Open, $ticket->status);
    }

    public function test_an_applicant_never_sees_the_internal_notes(): void
    {
        $id = $this->openTicket();
        $officer = $this->user('support-officer');

        $this->actingAs($officer)->postJson("/api/v1/tickets/{$id}/reply", [
            'body' => 'INTERNAL: the agent may have pocketed this.',
            'is_internal' => true,
        ])->assertOk();

        $this->actingAs($officer)->postJson("/api/v1/tickets/{$id}/reply", [
            'body' => 'We are looking into it and will come back to you.',
        ])->assertOk();

        $response = $this->actingAs($this->portalUser)->getJson("/api/v1/tickets/{$id}")->assertOk();

        $this->assertStringNotContainsString('pocketed', $response->getContent());
        $this->assertCount(2, $response->json('data.messages'));

        // Staff see the whole thread.
        $staffView = $this->actingAs($officer)->getJson("/api/v1/tickets/{$id}")->assertOk();
        $this->assertCount(3, $staffView->json('data.messages'));
    }

    public function test_an_applicant_cannot_post_an_internal_note(): void
    {
        $id = $this->openTicket();

        $this->actingAs($this->portalUser)
            ->postJson("/api/v1/tickets/{$id}/reply", [
                'body' => 'Trying to hide this.',
                'is_internal' => true,
            ])
            ->assertOk();

        $message = SupportTicketMessage::latest('id')->firstOrFail();

        $this->assertFalse($message->is_internal, 'An applicant cannot write an internal note.');
    }

    public function test_an_applicant_cannot_read_another_applicants_ticket(): void
    {
        $id = $this->openTicket();

        $other = Applicant::factory()->for($this->company)->create(['branch_id' => $this->branch->id]);
        $otherUser = tap(User::factory()->create([
            'company_id' => $this->company->id,
            'user_type' => 'applicant',
        ]))->assignRole('applicant');
        $other->update(['user_id' => $otherUser->id]);

        $this->actingAs($otherUser)->getJson("/api/v1/tickets/{$id}")->assertForbidden();
    }

    public function test_a_ticket_becomes_overdue_when_nobody_answers(): void
    {
        $id = $this->openTicket();

        SupportTicket::where('uuid', $id)->update(['due_at' => now()->subDay()]);

        $ticket = SupportTicket::where('uuid', $id)->firstOrFail();
        $this->assertTrue($ticket->isOverdue());

        $overdue = $this->actingAs($this->user('support-officer'))
            ->getJson('/api/v1/tickets?overdue=1')
            ->assertOk()
            ->json('data');

        $this->assertCount(1, $overdue);
    }

    public function test_a_ticket_waiting_on_the_applicant_does_not_read_as_overdue(): void
    {
        $id = $this->openTicket();

        $this->actingAs($this->user('support-officer'))
            ->postJson("/api/v1/tickets/{$id}/reply", ['body' => 'Could you send the receipt?'])
            ->assertOk();

        SupportTicket::where('uuid', $id)->update(['due_at' => now()->subDay()]);

        $this->assertFalse(SupportTicket::where('uuid', $id)->firstOrFail()->isOverdue());
    }

    public function test_resolving_a_ticket_needs_an_explanation(): void
    {
        $id = $this->openTicket();
        $officer = $this->user('support-officer');

        $this->actingAs($officer)
            ->postJson("/api/v1/tickets/{$id}/status", ['status' => 'resolved'])
            ->assertStatus(422);

        $this->actingAs($officer)
            ->postJson("/api/v1/tickets/{$id}/status", [
                'status' => 'resolved',
                'note' => 'The payment was found and posted; the balance is now correct.',
            ])
            ->assertOk()
            ->assertJsonPath('data.status', TicketStatus::Resolved->value);

        $this->assertGreaterThan(
            0,
            NotificationMessage::where('template_code', 'ticket_resolved')->count(),
        );
    }

    public function test_a_closed_ticket_cannot_be_replied_to(): void
    {
        $id = $this->openTicket();
        $officer = $this->user('support-officer');

        $this->actingAs($officer)->postJson("/api/v1/tickets/{$id}/status", [
            'status' => 'resolved',
            'note' => 'Sorted.',
        ])->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/tickets/{$id}/reply", ['body' => 'One more thing.'])
            ->assertStatus(422);
    }

    public function test_a_ticket_message_cannot_be_edited(): void
    {
        $this->openTicket();

        $message = SupportTicketMessage::firstOrFail();

        $this->expectException(RuntimeException::class);

        $message->body = 'That is not what I said.';
        $message->save();
    }

    public function test_staff_raising_a_ticket_must_say_who_it_is_about(): void
    {
        $this->actingAs($this->user('support-officer'))
            ->postJson('/api/v1/tickets', [
                'subject' => 'Called in about a delay',
                'category' => 'process_delay',
                'body' => 'Rang the office asking about their visa.',
            ])
            ->assertStatus(422);
    }

    public function test_reading_tickets_needs_the_permission(): void
    {
        $this->openTicket();

        $this->actingAs($this->user('data-entry-officer'))
            ->getJson('/api/v1/tickets')
            ->assertForbidden();
    }
}
