#!perl
use Cassandane::Tiny;

use Data::GUID qw(guid_string);

# Attempt to cover all (well, many) cases of contact access to show that in no
# case do we munge the UIDs.
#
# This test file generates many tests, with multiple combinations of how cards
# can be written and read.  The tests will all *start* with
# "test_contact_member_uid_matrix_", and the rest combine the keys from the
# hashes below.  The individual assigments to these hashes explain the keys.
#
# For more details, search for "UPDATE TESTS" and "CREATE TESTS" in the source
# below.

my %MAKE_TARGET_UID;  # ($test_case) -> ($uid) -- create a uid
my %CREATE_VIA;       # ($test_case, $uid) -> ($group_uid) -- create group with given member
my %UPDATE_VIA;       # ($test_case, $group_uid, $uid) -- add member to group
my %GET_VIA;          # ($test_case, $group_uid) -> (\@uids) -- get group member uids

# Get a UID that refers to an individual card in the user's own addressbooks,
# with no urn:uuid: prefix.
$MAKE_TARGET_UID{ON} = sub ($test_case)
{
    my $user = $test_case->default_user;
    my $abook = $user->addressbooks->create;

    my $uid = guid_string();
    my $got = $abook->create_card({ uid => $uid })->uid;

    $test_case->assert_str_equals($got, $uid, "uid not altered during creation");

    return $got;
};

# Get a UID that refers to an individual card in the user's own addressbooks,
# with a urn:uuid: prefix.
$MAKE_TARGET_UID{OP} = sub ($test_case)
{
    my $user = $test_case->default_user;
    my $abook = $user->addressbooks->create;

    my $uid = "urn:uuid:" . guid_string();
    my $got = $abook->create_card({ uid => $uid })->uid;

    $test_case->assert_str_equals($got, $uid, "uid not altered during creation");

    return $got;
};

# Get a UID that refers to an individual card in a shared addressbook, with no
# urn:uuid: prefix.
$MAKE_TARGET_UID{SN} = sub ($test_case)
{
    my $user = $test_case->default_user;

    my $sharer = $test_case->{instance}->create_user('other');
    my $abook = $sharer->addressbooks->create;
    $abook->share_with($user => [ qw( mayRead ) ]);

    my $uid = guid_string();
    my $got = $abook->create_card({ uid => $uid })->uid;

    $test_case->assert_str_equals($got, $uid, "uid not altered during creation");

    return $got;
};

# Get a UID that refers to an individual card in a shared addressbook, with a
# urn:uuid: prefix.
$MAKE_TARGET_UID{SP} = sub ($test_case)
{
    my $user = $test_case->default_user;

    my $sharer = $test_case->{instance}->create_user('other');
    my $abook = $sharer->addressbooks->create;
    $abook->share_with($user => [ qw( mayRead ) ]);

    my $uid = "urn:uuid:" . guid_string();
    my $got = $abook->create_card({ uid => $uid })->uid;

    $test_case->assert_str_equals($got, $uid, "uid not altered during creation");

    return $got;
};

# Get a UID that is a void reference and has no urn:uuid: prefix.
$MAKE_TARGET_UID{VN} = sub { return guid_string() };

# Get a UID that is a void reference and has a urn:uuid: prefix.
$MAKE_TARGET_UID{VP} = sub { return "urn:uuid:" . guid_string() };

# Create the group with CardDAV, creating a 3.0 Apple-style group
$CREATE_VIA{CD3} = sub ($test_case, $uid)
{
    my $user = $test_case->default_user;
    my $carddav = $user->carddav;
    my $group_uid = guid_string();

    my $encoded_uid = $uid =~ /:/
                    ? qq{urn:uuid:"$uid"}
                    : qq{urn:uuid:$uid};

    my $href = "Default/$group_uid.vcf";
    my $card = <<~"EOF";
    BEGIN:VCARD
    VERSION:3.0
    X-ADDRESSBOOKSERVER-KIND:group
    UID:$group_uid
    N:;Example vCard3-Apple;;;
    FN:Example vCard3-Apple Group
    X-ADDRESSBOOKSERVER-MEMBER:$encoded_uid
    END:VCARD
    EOF

    $card =~ s/\r?\n/\r\n/gs;
    $carddav->Request('PUT', $href, $card, 'Content-Type' => 'text/vcard');

    return $group_uid;
};

# Create the group with CardDAV, creating a 4.0 standard group
$CREATE_VIA{CD4} = sub ($test_case, $uid)
{
    my $user = $test_case->default_user;
    my $carddav = $user->carddav;
    my $group_uid = guid_string();

    my $href = "Default/$group_uid.vcf";
    my $card = <<~"EOF";
    BEGIN:VCARD
    VERSION:4.0
    KIND:group
    UID:$group_uid
    FN:Example vCard4 Group
    MEMBER:$uid
    END:VCARD
    EOF

    $card =~ s/\r?\n/\r\n/gs;
    $carddav->Request('PUT', $href, $card, 'Content-Type' => 'text/vcard');

    return $group_uid;
};

# Create the group with JMAP, ContactCard/set
$CREATE_VIA{JCC} = sub ($test_case, $uid)
{
    my $res = $test_case->default_user->jmap->request([[
        'ContactCard/set',
        {
            create => {
                new => {
                    kind => 'group',
                    name => { full => 'Example ContactCard Group' },
                    members => { $uid => \1 },
                    version => '1.0',
                    '@type' => 'Card',
                },
            },
        },
    ]]);

    $res->assert_successful;

    my $group_uid = $res->single_sentence('ContactCard/set')
                         ->arguments
                         ->{created}{new}{uid};

    return "$group_uid";
};

# Create the group with JMAP, ContactGroup/set
$CREATE_VIA{JCG} = sub ($test_case, $uid)
{
    my $res = $test_case->default_user->jmap->request([[
        'ContactGroup/set',
        {
            create => {
                new => {
                    name => 'Example ContactGroup',
                    contactIds => [ $uid ],
                },
            },
        },
    ]]);

    $res->assert_successful;

    my $group_uid = $res->single_sentence('ContactGroup/set')
                        ->arguments
                        ->{created}{new}{uid};

    return "$group_uid";
};

my sub _get_carddav ($user, $group_uid, $version) {
    my $carddav = $user->carddav;

    my $res = $carddav->Request(
        GET => "Default/$group_uid.vcf",
        undef,
        'Accept' => "text/vcard; version=$version",
    );

    require Text::VCardFast;
    my $struct = Text::VCardFast::vcard2hash($res->{content});

    $struct || Carp::confess("can't parse content");

    $struct->{objects}->@* == 1
        || Carp::confess("didn't get exactly one object when parsing vcard");

    my $card = $struct->{objects}[0];

    my @ids;

    # The encode/decode is more complex here, but we aren't using anything that
    # would trigger it just yet.  If we start, we need to do more here.
    push @ids, map {; $_->{value} =~ s/^urn:uuid://r =~ s/(\A"|"\z)//gr }
               $card->{properties}{'x-addressbookserver-member'}->@*;

    push @ids, map {; $_->{value} }
               $card->{properties}{'member'}->@*;


    return \@ids;
}

$UPDATE_VIA{JCC} = sub ($test_case, $group_uid, $member_uid)
{
    my $user = $test_case->default_user;

    my $get_res = $user->jmap->request([[ 'ContactCard/get', {} ]]);

    $get_res->assert_successful;

    my ($group) = grep {; $_->{uid} eq $group_uid }
                  $get_res->single_sentence('ContactCard/get')->arguments->{list}->@*;

    my $set_res = $user->jmap->request([[ 'ContactCard/set' => {
        update => { $group->{id} => { "members/$member_uid" => jtrue } }
    } ]]);

    $set_res->assert_successful;
    return;
};

# This updates the group membership with ContactCard/set, but instead of
# patching the group with { "members/$member_uid": true }, it will replace the
# entire members property.
$UPDATE_VIA{JCE} = sub ($test_case, $group_uid, $member_uid)
{
    my $user = $test_case->default_user;

    my $get_res = $user->jmap->request([[ 'ContactCard/get', {} ]]);

    $get_res->assert_successful;

    my ($group) = grep {; $_->{uid} eq $group_uid }
                  $get_res->single_sentence('ContactCard/get')->arguments->{list}->@*;

    my $new_members = {
        $member_uid => jtrue,
        (map {; $_ => jtrue } keys $group->{members}->%*),
    };

    my $set_res = $user->jmap->request([[ 'ContactCard/set' => {
        update => { $group->{id} => { members => $new_members } }
    } ]]);

    $set_res->assert_successful;
    return;
};

$UPDATE_VIA{JCG} = sub ($test_case, $group_uid, $member_uid)
{
    my $user = $test_case->default_user;

    my $get_res = $user->jmap->request([[ 'ContactGroup/get', {
        ids => [ $group_uid ],
    } ]]);

    $get_res->assert_successful;

    my ($group) = $get_res->single_sentence('ContactGroup/get')->arguments->{list}[0];

    $group || die "couldn't ContactGroup/get group $group_uid!?";

    my @contact_ids = $group->{contactIds}->@*;

    # Already in there?  Weird.  Since we have no cases that *should* trigger
    # this, it's fatal. -- rjbs, 2026-02-06
    if (grep {; $_ eq $member_uid } @contact_ids) {
        die "Unexpected case: trying to add member, but they're already there";
    }

    my $set_res = $user->jmap->request([[ 'ContactGroup/set' => {
        update => { $group_uid => { contactIds => [ @contact_ids, $member_uid ] } }
    } ]]);

    $set_res->assert_successful;
    return;
};

$GET_VIA{CD3} = sub ($test_case, $group_uid)
{
    _get_carddav($test_case->default_user, $group_uid, '3.0');
};

$GET_VIA{CD4} = sub ($test_case, $group_uid)
{
    _get_carddav($test_case->default_user, $group_uid, '4.0');
};

# Get the group with JMAP, using ContactCard/get
$GET_VIA{JCC} = sub ($test_case, $group_uid)
{
    my $res = $test_case->default_user->jmap->request([[ 'ContactCard/get', {} ]]);

    $res->assert_successful;

    my ($group) = grep {; $_->{uid} eq $group_uid }
                  $res->single_sentence('ContactCard/get')->arguments->{list}->@*;

    $test_case->assert_not_null($group, "group not found via ContactCard/get");

    return [ keys $group->{members}->%* ];
};

# Get the group with JMAP, using ContactGroup/get
$GET_VIA{JCG} = sub ($test_case, $group_uid)
{
    my $res = $test_case->default_user->jmap->request([[ 'ContactGroup/get', {} ]]);

    $res->assert_successful;

    my ($group) = grep {; $_->{uid} eq $group_uid }
                  $res->single_sentence('ContactGroup/get')->arguments->{list}->@*;

    $test_case->assert_not_null($group, "group not found via ContactGroup/get");

    return $group->{contactIds};
};

# CREATE TESTS
#
# These tests will, in order:
# 1.  pick a member UID, possibly by creating an individual contact
# 2.  create a group with that UID as a group member
# 3.  retrieve the group's effective member UIDs
# 4.  assert that the set of UIDs is exactly { member UID }
#
# Steps 1-3 are pluggable using the global hashes you'll see in this loop.
# Each test will have its own name, so it can be run individually, and it will
# be in the form:
#
#   test_Contact_member_uid_matrix_tXXX_cYYY_gZZZ
#
# Where t, c, and g prefix the "uid type", "create group" and "get group" keys.
for my $uid_type (sort keys %MAKE_TARGET_UID) {
    for my $create (sort keys %CREATE_VIA) {
        CASE: for my $get (sort keys %GET_VIA) {
            if ($create eq 'JCG' && $uid_type =~ /P$/) {
              # The (doomed) ContactGroup API will not properly re-prefix
              # already-prefixed UIDs when writing a member-ish property.  Skip
              # these tests, because the API will go away.
              next CASE;
            }

            if ($create eq 'CD3' && $get eq 'JCG' && $uid_type =~ /P$/) {
              # Right now, ContactGroup/get will not fully 6868-decode
              # Apple-encoded X-ABS-M properties.  This API is doomed, and I
              # believe this has long been broken, so we're not going to fix
              # it.
              next CASE;
            }

            my $name = "test_contact_member_uid_matrix_t${uid_type}_c${create}_g${get}";

            Sub::Install::install_sub({
                code => sub :min_version_3_12 :JMAPExtensions ($self, @)
                {
                    $self->_do_create_test($uid_type, $create, $get);
                },
                as => $name,
            });
        }
    }
}

sub _do_create_test ($self, $uid_type, $create, $get)
{
    my $user = $self->default_user;

    my $member_uid  = q{} . $MAKE_TARGET_UID{$uid_type}->($self);
    my $group_uid   = $CREATE_VIA{$create}->($self, $member_uid);

    my $got = $GET_VIA{$get}->($self, $group_uid);

    $self->assert_cmp_deeply(
        [ $member_uid ],
        $got,
        "membership unmunged in create test: $uid_type/$create/$get",
    );
}

# UPDATE TESTS
#
# These tests will, in order:
# 1.  generate a UUID to use as a meaningless group member UID ("starter uid")
# 2.  pick a member UID, possibly by creating an individual contact
# 3.  create a group with only the starter UID
# 4.  add the member UID to the created group
# 5.  retrieve the group's effective member UIDs
# 6.  assert that the set of UIDs is exactly { starter UID, member UID }
#
# Steps 2-5 are pluggable using the global hashes you'll see in this loop.
# Each test will have its own name, so it can be run individually, and it will
# be in the form:
#
#   test_Contact_member_uid_matrix_tWWW_cXXX_uYYY_gZZZ
#
# Where t, c, u, and g prefix the "uid type", "create group", "update group",
# and "get group" keys.
for my $uid_type (sort keys %MAKE_TARGET_UID) {
    for my $create (sort keys %CREATE_VIA) {
        for my $update (sort keys %UPDATE_VIA) {
            CASE: for my $get (sort keys %GET_VIA) {
                if (($create eq 'JCG' || $update eq 'JCG') && $uid_type =~ /P$/) {
                  # The (doomed) ContactGroup API will not properly re-prefix
                  # already-prefixed UIDs when writing a member-ish property.  Skip
                  # these tests, because the API will go away.
                  next CASE;
                }

                my $name = "test_contact_member_uid_matrix_t${uid_type}_c${create}_u${update}_g${get}";

                Sub::Install::install_sub({
                    code => sub :min_version_3_12 :JMAPExtensions ($self, @)
                    {
                        $self->_do_update_test($uid_type, $create, $update, $get);
                    },
                    as => $name,
                });
            }
        }
    }
}

sub _do_update_test ($self, $uid_type, $create, $update, $get)
{
    my $user = $self->default_user;

    # This UID is just so we can create a group to which we'll add the
    # $member_uid afterward via the UPDATE_VIA callback.
    my $starter_uid = guid_string();
    my $group_uid   = $CREATE_VIA{$create}->($self, $starter_uid);

    my $member_uid  = q{} . $MAKE_TARGET_UID{$uid_type}->($self);

    $UPDATE_VIA{$update}->($self, $group_uid, $member_uid);

    my $got = $GET_VIA{$get}->($self, $group_uid);

    $self->assert_cmp_deeply(
        bag($member_uid, $starter_uid),
        $got,
        "membership unmunged in update test: $uid_type/$create/$update/$get",
    );
}
