Improve handling of base offset shifts

This fixes a bug with the transition time of the Samoan base offset
change (UTC-10 -> UTC+14), relating to the fact that it occurred during
DST.

When cleaning that up, I discovered a cleaner way to handle the issue of
Portugal's base offset shift without a corresponding total offset shift,
obviating the need for a second traversal of the transition list.
diff --git a/dateutil/test/test_tz.py b/dateutil/test/test_tz.py
index fb0cd44..d5fdb8d 100644
--- a/dateutil/test/test_tz.py
+++ b/dateutil/test/test_tz.py
@@ -2032,9 +2032,8 @@
 
 
 @pytest.mark.tzfile
-@pytest.mark.xfail
 def test_samoa_transition():
-    # utcoffset() is erroneously returning +14:00 an hour early (GH #812)
+    # utcoffset() was erroneously returning +14:00 an hour early (GH #812)
     APIA = tz.gettz('Pacific/Apia')
     dt = datetime(2011, 12, 29, 23, 59, tzinfo=APIA)
     assert dt.utcoffset() == timedelta(hours=-10)
diff --git a/dateutil/tz/tz.py b/dateutil/tz/tz.py
index 3caac67..101f446 100644
--- a/dateutil/tz/tz.py
+++ b/dateutil/tz/tz.py
@@ -657,37 +657,44 @@
         # isgmt are off, so it should be in wall time. OTOH, it's
         # always in gmt time. Let me know if you have comments
         # about this.
-        laststdoffset = None
+        lastdst = None
+        lastoffset = None
+        lastdstoffset = None
+        lastbaseoffset = None
         out.trans_list = []
+
         for i, tti in enumerate(out.trans_idx):
-            if not tti.isdst:
-                offset = tti.offset
-                laststdoffset = offset
-            else:
-                if laststdoffset is not None:
-                    # Store the DST offset as well and update it in the list
-                    tti.dstoffset = tti.offset - laststdoffset
-                    out.trans_idx[i] = tti
+            offset = tti.offset
+            dstoffset = 0
 
-                offset = laststdoffset or 0
+            if lastdst is not None:
+                if tti.isdst:
+                    if not lastdst:
+                        dstoffset = offset - lastoffset
 
-            out.trans_list.append(out.trans_list_utc[i] + offset)
+                    if not dstoffset and lastdstoffset:
+                        dstoffset = lastdstoffset
 
-        # In case we missed any DST offsets on the way in for some reason, make
-        # a second pass over the list, looking for the /next/ DST offset.
-        laststdoffset = None
-        for i in reversed(range(len(out.trans_idx))):
-            tti = out.trans_idx[i]
-            if tti.isdst:
-                if not (tti.dstoffset or laststdoffset is None):
-                    tti.dstoffset = tti.offset - laststdoffset
-            else:
-                laststdoffset = tti.offset
+                    tti.dstoffset = datetime.timedelta(seconds=dstoffset)
+                    lastdstoffset = dstoffset
 
-            if not isinstance(tti.dstoffset, datetime.timedelta):
-                tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset)
+            # If a time zone changes its base offset during a DST transition,
+            # then you need to adjust by the previous base offset to get the
+            # transition time in local time. Otherwise you use the current
+            # base offset. Ideally, I would have some mathematical proof of
+            # why this is true, but I haven't really thought about it enough.
+            baseoffset = offset - dstoffset
+            adjustment = baseoffset
+            if (lastbaseoffset is not None and baseoffset != lastbaseoffset
+                    and tti.isdst != lastdst):
+                # The base DST has changed
+                adjustment = lastbaseoffset
 
-            out.trans_idx[i] = tti
+            lastdst = tti.isdst
+            lastoffset = offset
+            lastbaseoffset = baseoffset
+
+            out.trans_list.append(out.trans_list_utc[i] + adjustment)
 
         out.trans_idx = tuple(out.trans_idx)
         out.trans_list = tuple(out.trans_list)