package Test::BDD::Cucumber::StepContext;
{
  $Test::BDD::Cucumber::StepContext::VERSION = '0.17';
}
use Moose;

use List::Util qw( first );

=head1 NAME

Test::BDD::Cucumber::StepContext - Data made available to step definitions

=head1 VERSION

version 0.17

=head1 DESCRIPTION

The coderefs in Step Definitions have a single argument passed to them, a
C<Test::BDD::Cucumber::StepContext> object. This is an attribute-only class,
populated by L<Test::BDD::Cucumber::Executor>.

=head1 ATTRIBUTES

=head2 columns

If the step-specific data supplied is a table, the this attribute will contain
the column names in the order they appeared.

=cut

has 'columns' => ( is => 'ro', isa => 'ArrayRef' );

=head2 _data

Step-specific data. Will either be a text string in the case of a """ string, or
an arrayref of hashrefs if the step had an associated table.

See the C<data> method below.

=cut

has '_data'     => ( is => 'ro', isa => 'Str|ArrayRef', init_arg => 'data', default => '' );

=head2 stash

A hash of hashes, containing three keys, C<feature>, C<scenario> and C<step>.
The stash allows you to persist data across features, scenarios, or steps
(although the latter is there for completeness, rather than having any useful
function).

=cut

has 'stash'    => ( is => 'ro', required => 1, isa => 'HashRef' );

=head2 feature

=head2 scenario

=head2 step

Links to the L<Test::BDD::Cucumber::Model::Feature>,
L<Test::BDD::Cucumber::Model::Scenario>, and L<Test::BDD::Cucumber::Model::Step>
objects respectively.

=cut

has 'feature'  => ( is => 'ro', required => 1, isa => 'Test::BDD::Cucumber::Model::Feature' );
has 'scenario' => ( is => 'ro', required => 1, isa => 'Test::BDD::Cucumber::Model::Scenario' );
has 'step'     => ( is => 'ro', required => 0, isa => 'Test::BDD::Cucumber::Model::Step' );

=head2 verb

The lower-cased verb a Step Definition was called with.

=cut

has 'verb'     => ( is => 'ro', required => 1, isa => 'Str' );

=head2 text

The text of the step, minus the verb. Placeholders will have already been
multiplied out at this point.

=cut

has 'text'     => ( is => 'ro', required => 1, isa => 'Str', default => '' );

=head2 harness

The L<Test::BDD::Cucumber::Harness> harness being used by the executor.

=cut

has 'harness'  => ( is => 'ro', required => 1, isa => 'Test::BDD::Cucumber::Harness' );

=head2 matches

Any matches caught by the Step Definition's regex. These are also available as
C<$1>, C<$2> etc as appropriate.

=cut

has '_matches'  => ( is => 'rw', isa => 'ArrayRef', init_arg => 'matches', default => sub { [] } );

has 'transformers' => ( is => 'ro', isa => 'ArrayRef', predicate => 'has_transformers', );

has '_transformed_matches' => ( is => 'ro', isa => 'ArrayRef', lazy => 1, builder => '_build_transformed_matches', clearer => '_clear_transformed_matches', );

has '_transformed_data' => ( is => 'ro', isa => 'Str|ArrayRef', lazy => 1, builder => '_build_transformed_data', clearer => '_clear_transformed_data', );

=head2 is_hook

The harness processing the output can decide whether to shop information for
this step which is actually an internal hook, i.e. a Before or After step

=cut

has 'is_hook' => ( is => 'ro', isa => 'Bool', lazy => 1, builder => '_build_is_hook' );

=head1 METHODS

=head2 background

Boolean for "is this step being run as part of the background section?".
Currently implemented by asking the linked Scenario object...

=cut

sub background { my $self = shift; return $self->scenario->background }

=head2 data

See the C<_data> attribute above.

Calling this method will return either the """ string, or a possibly Transform-ed
set of table data.

=cut

sub data
{
    my $self = shift;

    if ( @_ )
    {
        $self->_data( @_ );
        $self->_clear_transformed_data;
        return;
    }

    return $self->_transformed_data;
}

=head2 matches

See the C<_matches> attribute above.

Call this method will return the possibly Transform-ed matches .

=cut

sub matches
{
    my $self = shift;

    if ( @_ )
    {
        $self->_matches( @_ );
        $self->_clear_transformed_matches;
        return;
    }

    return $self->_transformed_matches;
}

=head2 transform

Used internally to transform data and placeholders, but it can also be called
from within your Given/When/Then code.

=cut

sub transform
{
    my $self = shift;
    my $value = shift;

    defined $value or return $value;
        
    TRANSFORM:
    for my $transformer ( @{ $self->transformers } )
    {
        # uses the same magic as other steps
        # and puts any matches into $1, $2, etc.
        # and calls the Transform step
        if ( $value =~ s/$transformer->[0]/$transformer->[1]->( $self )/e )
        {
            # if we matched then stop processing this match
            return $value;
        }
    }

    # if we're here, the value will be returned unchanged
    return $value;
}

# the builder for the is_hook attribute
sub _build_is_hook
{
    my $self = shift;

    return ( $self->verb eq 'before' or $self->verb eq 'after' ) ? 1 : 0;
}

# the builder for the _transformed_matches attribute
sub _build_transformed_matches
{
    my $self = shift;

    my @transformed_matches = @{ $self->_matches };

    # this stops it recursing forever...
    # and only Transform if there are any to process
    if ( $self->verb ne 'transform'
        and $self->has_transformers )
    {
        @transformed_matches = map { $_ = $self->transform( $_ ) } @transformed_matches;
    }

    return \@transformed_matches;
}

# the builder for the _transformed_data attribute
sub _build_transformed_data
{
    my $self = shift;

    my $transformed_data = $self->_data;

    # again stop recursing
    # only transform table data
    # and only Transform if there are any to process
    if ( $self->verb ne 'transform'
        and ref $transformed_data
        and $self->has_transformers )
    {
        # build the string that a Transform is looking for
        # table:column1,column2,column3
        my $table_text = 'table:' . join( ',', @{ $self->columns } );

        if ( my $transformer = first { $table_text =~ $_->[0] } @{ $self->transformers } )
        {
            # call the Transform step
            $transformer->[1]->( $self, $transformed_data );
        }
    }

    return $transformed_data;
}

=head1 AUTHOR

Peter Sergeant C<pete@clueball.com>

=head1 LICENSE

Copyright 2011, Peter Sergeant; Licensed under the same terms as Perl

=cut

1;
