#!/usr/bin/env perl

# Qisutu - Open Source Ticket System
# Copyright (C) 2026 Franziska Steps
# Qisutu - Kim-KI, https://qisutu.de
#
# This file is part of Qisutu.
#
# Qisutu is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Qisutu is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Qisutu. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-FileCopyrightText: 2026 Franziska Steps
# SPDX-License-Identifier: AGPL-3.0-or-later

use strict;
use warnings;
use utf8;

use File::Spec;
use FindBin;
use Test::More;

use lib File::Spec->catdir( $FindBin::Bin, '..', 'core', 'system' );
use lib File::Spec->catdir( $FindBin::Bin, '..', 'core', 'module' );

use QisutuCustomerAutoResponse;
use QisutuCustomerAutoResponseTemplates;
use QisutuMail;

my @Type = map { $_->{type} } @{ QisutuCustomerAutoResponse->ResponseTypes() };
is_deeply(
    \@Type,
    [ qw(customer_ticket_created customer_ticket_reply incoming_email_rejected closed_ticket_follow_up) ],
    'only the four agreed customer-originated events are available',
);
is(
    scalar @{ QisutuCustomerAutoResponseTemplates->Languages() },
    11,
    'automatic responses support all eleven Qisutu languages',
);
for my $Language ( @{ QisutuCustomerAutoResponseTemplates->Languages() } ) {
    is(
        scalar @{ QisutuCustomerAutoResponseTemplates->Templates( Language => $Language->{code} ) },
        4,
        "$Language->{code} provides all four automatic responses",
    );
}

my %Placeholder = map { ( $_->{placeholder} || '' ) => 1 } @{ QisutuCustomerAutoResponse->PlaceholderList() };
ok( $Placeholder{'{{CustomerUser.Firstname}}'}, 'customer-user placeholders are offered' );
ok( $Placeholder{'{{Ticket.LinkHTML}}'}, 'customer ticket link placeholder is offered' );
ok( $Placeholder{'{{Ticket.ArticleBody[15]}}'}, 'customer notification settings offer the configurable article-line placeholder' );
ok( $Placeholder{'{{Incoming.Subject}}'}, 'incoming-mail placeholders are offered for rejection responses' );
ok( !$Placeholder{'{{Agent.FullName}}'}, 'irrelevant agent-recipient placeholders are not offered' );

{
    package Local::CustomerAutoResponseDB;

    sub new {
        return bless { Event => {}, Updated => [] }, shift;
    }

    sub Error {
        return '';
    }

    sub Do {
        my ( $Self, $SQL, @Bind ) = @_;

        if ( $SQL =~ m{INSERT\s+IGNORE\s+INTO\s+customer_auto_response_event_log}si ) {
            $Self->{Event}->{ $Bind[0] . '|' . $Bind[4] } = 1;
        }
        if ( $SQL =~ m{UPDATE\s+customer_auto_response_template}si ) {
            push @{ $Self->{Updated} }, \@Bind;
        }
        return 1;
    }

    sub SelectAll {
        return [];
    }

    sub SelectRow {
        my ( $Self, $SQL, @Bind ) = @_;

        if ( $SQL =~ m{FROM\s+customer_auto_response_template}si ) {
            my $Type = $Bind[0] || '';
            my $Language = $Bind[1] || 'de';
            return {
                response_type => $Type,
                language      => $Language,
                name          => 'Test response',
                subject       => $Type eq 'incoming_email_rejected'
                    ? ( $Language eq 'en' ? 'Rejected: {{Incoming.Subject}}' : 'Nicht angenommen: {{Incoming.Subject}}' )
                    : ( $Language eq 'pt-BR' ? 'Recebido: {{Ticket.Number}} – {{Ticket.Title}}' : 'Empfangen: {{Ticket.Number}} – {{Ticket.Title}}' ),
                body_html     => $Type eq 'incoming_email_rejected'
                    ? '<p>{{Incoming.FromName}}: {{Incoming.Subject}}</p>'
                    : ( $Language eq 'pt-BR'
                        ? '<p>Olá {{CustomerUser.Firstname}}</p><p>{{Ticket.LinkHTML}}</p><p>{{Ticket.ArticleBody[15]}}</p>'
                        : '<p>Hallo {{CustomerUser.Firstname}}</p><p>{{Ticket.LinkHTML}}</p><p>{{Ticket.ArticleBody[15]}}</p>' ),
                active        => 1,
            };
        }

        if ( $SQL =~ m{FROM\s+information_schema\.(?:COLUMNS|STATISTICS)}si ) {
            return { object_count => 1 };
        }

        if ( $SQL =~ m{FROM\s+customer_auto_response_event_log}si ) {
            return $Self->{Event}->{ $Bind[0] . '|' . $Bind[1] } ? { id => 1 } : undef;
        }

        if ( $SQL =~ m{FROM\s+ticket\s+t}si ) {
            return {
                id                        => 42,
                ticket_number             => '2026000042',
                title                     => 'Drucker & Scanner',
                queue_id                  => 1,
                queue_name                => 'Posteingang',
                queue_full_name           => 'Support::Posteingang',
                state_name                => 'Offen',
                priority_name             => '3 normal',
                customer_name             => 'Beispiel GmbH',
                customer_number           => 'K-100',
                customer_user_id           => 17,
                customer_user_login       => 'klara',
                customer_user_email       => 'klara@example.test',
                customer_user_firstname   => 'Klara',
                customer_user_lastname    => 'Kundin',
                system_email_name         => 'Qisutu Support',
                system_email              => 'support@example.test',
            };
        }

        if ( $SQL =~ m{FROM\s+ticket_article}si ) {
            return {
                id             => 7,
                ticket_id      => 42,
                article_number => 1,
                sender_type    => 'customer',
                content_type   => 'text/plain',
                body           => join( "\n", map { "Customer line $_" } 1 .. 20 ),
            };
        }

        if ( $SQL =~ m{FROM\s+customer_user\s+cu}si ) {
            return { language => 'pt-BR' };
        }

        if ( $SQL =~ m{FROM\s+user_account\s+ua}si ) {
            return { language => 'en' };
        }

        if ( $SQL =~ m{FROM\s+smtp_account}si ) {
            return {
                id            => 1,
                active        => 1,
                smtp_host     => 'smtp.example.test',
                smtp_username => 'mailer@example.test',
            };
        }

        if ( $SQL =~ m{FROM\s+system_setting}si ) {
            return;
        }

        die "Unexpected SelectRow SQL in customer-auto-response test: $SQL";
    }
}

my $Config = {
    Paths => {
        Config        => File::Spec->catdir( $FindBin::Bin, '..', 'core', 'config' ),
        SettingConfig => File::Spec->catdir( $FindBin::Bin, '..', 'core', 'config', 'settings' ),
    },
    System => {
        Name       => 'Qisutu Test',
        BaseURL    => 'https://support.example.test/qisutu',
        TicketHook => 'Qisutu',
    },
    Language => { Default => 'de' },
};
my $DB = Local::CustomerAutoResponseDB->new();
my @Sent;

{
    no warnings 'redefine';
    local *QisutuMail::SMTPSend = sub {
        my ( $Self, %Param ) = @_;
        push @Sent, \%Param;
        return { Success => 1, Message => 'sent' };
    };

    my $Response = QisutuCustomerAutoResponse->new( Config => $Config, DB => $DB );
    ok(
        $Response->TemplateUpdate(
            ResponseType   => 'customer_ticket_reply',
            Subject        => 'Antwort {{Ticket.Number}}',
            BodyHTML       => '<p>Empfangen</p><script>alert(1)</script>',
            Active         => 1,
            ChangedByUserID => 9,
        ),
        'administrator can activate and edit an individual event template',
    );
    is( $DB->{Updated}->[-1]->[2], 1, 'active flag is persisted for the selected event' );
    unlike( $DB->{Updated}->[-1]->[1], qr{script}, 'CKEditor HTML is sanitized before storage' );

    ok(
        $Response->Send(
            ResponseType => 'customer_ticket_created',
            TicketID     => 42,
            ArticleID    => 7,
        ),
        'active ticket-created response is sent',
    );

    is( scalar @Sent, 1, 'exactly one email is sent for the event' );
    like( $Sent[0]->{Subject}, qr{\A\[Qisutu\#2026000042\]}, 'customer response subject contains a reply-safe ticket reference' );
    like( $Sent[0]->{Subject}, qr{Recebido}, 'the customer preference selects the Brazilian Portuguese template' );
    like( $Sent[0]->{Body}, qr{Olá Klara}, 'customer placeholder is rendered in the recipient language' );
    like( $Sent[0]->{Body}, qr{Page=CustomerTicketZoom&amp;TicketID=42}, 'ticket link points to the customer portal' );
    unlike( $Sent[0]->{Body}, qr{Page=AgentTicketZoom}, 'customer email never exposes an agent ticket link' );
    like( $Sent[0]->{Body}, qr{Customer line 15}, 'a customer notification includes the configured first fifteen article lines' );
    unlike( $Sent[0]->{Body}, qr{Customer line 16}, 'a customer notification excludes article lines after the configured limit' );
    unlike( $Sent[0]->{Body}, qr{<p><div}, 'the CKEditor paragraph wrapper does not produce invalid nested article-block HTML' );

    ok(
        !$Response->Send(
            ResponseType => 'customer_ticket_created',
            TicketID     => 42,
            ArticleID    => 7,
        ),
        'the same article event is not sent twice',
    );
    like( $Response->Error(), qr{already sent}, 'duplicate suppression is reported clearly' );
    is( scalar @Sent, 1, 'duplicate attempt does not call SMTP again' );

    ok(
        $Response->Send(
            ResponseType      => 'incoming_email_rejected',
            RecipientName     => 'Max Mustermann',
            RecipientEmail    => 'max@example.test',
            IncomingSubject   => 'Unerlaubte Anfrage',
            IncomingFromName  => 'Max Mustermann',
            IncomingFromEmail => 'max@example.test',
            IncomingToEmail   => 'support@example.test',
            ReplyToEmail      => 'support@example.test',
            EventKey          => 'reject:mailbox-1:uid-99',
            Headers           => { 'auto-submitted' => 'no' },
        ),
        'explicit postmaster rejection response is sent',
    );
    like( $Sent[1]->{Subject}, qr{Rejected: Unerlaubte Anfrage}, 'known incoming senders also receive their preferred language' );

    ok(
        !$Response->Send(
            ResponseType      => 'incoming_email_rejected',
            RecipientEmail    => 'robot@example.test',
            IncomingFromEmail => 'robot@example.test',
            Headers           => { 'auto-submitted' => 'auto-generated' },
            EventKey          => 'reject:mailbox-1:uid-100',
        ),
        'automatic senders do not receive rejection responses',
    );
    like( $Response->Error(), qr{suppressed}, 'automatic-sender suppression is reported' );
    is( scalar @Sent, 2, 'suppressed automatic message does not call SMTP' );
}

my $Root = File::Spec->catdir( $FindBin::Bin, '..' );
my $TicketSource = _Read( File::Spec->catfile( $Root, 'core', 'system', 'QisutuTicket.pm' ) );
my ($AgentCreate) = $TicketSource =~ m{sub\s+TicketCreateFromAgent\b(.*?)^sub\s+}ms;
unlike( $AgentCreate || '', qr{_CustomerAutoResponseSend}, 'agent-created tickets do not trigger an extra customer auto-response' );

my ($CustomerCreate) = $TicketSource =~ m{sub\s+TicketCreateFromCustomer\b(.*?)^sub\s+TicketCreateFromAgent\b}ms;
like( $CustomerCreate || '', qr{customer_ticket_created}, 'customer-created tickets trigger the receipt event' );

my ($EmailReply) = $TicketSource =~ m{sub\s+_TicketReplyCreateFromEmail\b(.*?)^sub\s+_PostmasterTicketDataResolve\b}ms;
like( $EmailReply || '', qr{closed_ticket_follow_up}, 'email replies to closed tickets use the dedicated event' );
like( $EmailReply || '', qr{customer_ticket_reply}, 'ordinary customer email replies use the receipt event' );

my $AdminTemplate = _Read( File::Spec->catfile( $Root, 'core', 'output', 'AdminCustomerAutoResponses.tt' ) );
like( $AdminTemplate, qr{qisutu-richtext}, 'each automatic-response edit view uses CKEditor' );
like( $AdminTemplate, qr{PlaceholderList}, 'the edit view presents its placeholder list' );
like( $AdminTemplate, qr{name="ResponseLanguage"}, 'the administration view offers a language selector' );

my $Release = _Read( File::Spec->catfile( $Root, 'release.conf' ) );
like( $Release, qr{^version=1[.]0[.]3$}m, 'automatic responses are included in release 1.0.3' );
like( $Release, qr{^database_version=1[.]0[.]2$}m, 'automatic responses are included in database version 1.0.2' );

my $Schema = _Read( File::Spec->catfile( $Root, 'install', 'sql', 'schema.sql' ) );
like( $Schema, qr{CREATE TABLE `customer_auto_response_template`}, 'fresh installations create automatic-response templates' );
like( $Schema, qr{CREATE TABLE `customer_auto_response_event_log`}, 'fresh installations create the duplicate-prevention log' );
like( $Schema, qr{customer_auto_response_template_type_language_unique}, 'automatic responses are unique per event and language' );

sub _Read {
    my ($File) = @_;
    open my $Handle, '<:encoding(UTF-8)', $File or die "Could not read $File: $!";
    local $/;
    my $Content = <$Handle>;
    close $Handle;
    return $Content;
}

done_testing();
